Browse Source

Merge branch 'master' into feature-proposal

# Conflicts:
#	src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs
#	src/Squidex/app/features/content/declarations.ts
#	src/Squidex/app/features/content/module.ts
#	src/Squidex/app/features/content/pages/content/content-page.component.html
#	src/Squidex/app/features/content/pages/content/content-page.component.ts
#	src/Squidex/app/features/content/pages/contents/contents-page.component.ts
#	src/Squidex/app/features/content/shared/content-item.component.html
#	src/Squidex/app/shared/services/contents.service.spec.ts
#	src/Squidex/app/shared/services/contents.service.ts
pull/285/head
Sebastian 9 years ago
parent
commit
ca2b82e0ce
  1. 5
      .drone.yml
  2. 2
      Dockerfile
  3. 27
      Dockerfile.build
  4. 1
      Squidex.ruleset
  5. 26
      libs/Dockerfile
  6. 4
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  7. 29
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs
  8. 17
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs
  9. 76
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  10. 9
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  11. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  12. 34
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  13. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  14. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  15. 186
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  16. 8
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  17. 28
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  18. 4
      src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs
  19. 4
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs
  20. 16
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs
  21. 5
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  22. 4
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  23. 109
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  24. 55
      src/Squidex.Domain.Apps.Entities/Contents/ContentVersionLoader.cs
  25. 5
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  26. 8
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  27. 9
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  28. 17
      src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs
  29. 8
      src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs
  30. 2
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  31. 2
      src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
  32. 5
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  33. 5
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  34. 5
      src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  35. 5
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
  36. 2
      src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  37. 32
      src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  38. 4
      src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  39. 3
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  40. 2
      src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  41. 5
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs
  42. 4
      src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  43. 68
      src/Squidex.Infrastructure/Caching/HttpRequestCache.cs
  44. 18
      src/Squidex.Infrastructure/Caching/IRequestCache.cs
  45. 43
      src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs
  46. 2
      src/Squidex.Infrastructure/CachingProviderBase.cs
  47. 24
      src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  48. 6
      src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs
  49. 2
      src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
  50. 17
      src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs
  51. 3
      src/Squidex.Infrastructure/FileExtensions.cs
  52. 26
      src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs
  53. 14
      src/Squidex.Infrastructure/Log/ILogProfilerSessionProvider.cs
  54. 24
      src/Squidex.Infrastructure/Log/NoopDisposable.cs
  55. 67
      src/Squidex.Infrastructure/Log/Profile.cs
  56. 58
      src/Squidex.Infrastructure/Log/ProfilerSession.cs
  57. 22
      src/Squidex.Infrastructure/Orleans/Bootstrap.cs
  58. 50
      src/Squidex.Infrastructure/Orleans/J{T}.cs
  59. 11
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  60. 2
      src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs
  61. 56
      src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs
  62. 2
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  63. 3
      src/Squidex.Shared/Squidex.Shared.csproj
  64. 5
      src/Squidex.Shared/Users/IUserResolver.cs
  65. 13
      src/Squidex.Shared/Users/UserExtensions.cs
  66. 1
      src/Squidex/Areas/Api/Controllers/ApiController.cs
  67. 10
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  68. 17
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  69. 21
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  70. 15
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  71. 27
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  72. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/AddAppLanguageDto.cs
  73. 25
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs
  74. 15
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  75. 26
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs
  76. 14
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs
  77. 9
      src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs
  78. 13
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  79. 25
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs
  80. 12
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  81. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppClientDto.cs
  82. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs
  83. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs
  84. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppLanguageDto.cs
  85. 13
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs
  86. 15
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  87. 4
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs
  88. 7
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  89. 8
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs
  90. 19
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
  91. 5
      src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  92. 7
      src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs
  93. 79
      src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs
  94. 4
      src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs
  95. 5
      src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
  96. 8
      src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs
  97. 3
      src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  98. 7
      src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs
  99. 7
      src/Squidex/Areas/Api/Controllers/LanguageDto.cs
  100. 3
      src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs

5
.drone.yml

@ -1,3 +1,8 @@
clone:
git:
image: plugins/git:next
pull: true
pipeline: pipeline:
test_pull_request: test_pull_request:
image: docker image: docker

2
Dockerfile

@ -1,7 +1,7 @@
# #
# Stage 1, Prebuild # Stage 1, Prebuild
# #
FROM squidex/aspnetcore-build-phantomjs:2.0.3-jessie as builder FROM squidex/aspnetcore-build-phantomjs-chromium:2.0.3-jessie-fix1 as builder
COPY src/Squidex/package.json /tmp/package.json COPY src/Squidex/package.json /tmp/package.json

27
Dockerfile.build

@ -1,30 +1,7 @@
FROM microsoft/aspnetcore-build:2.0.0 FROM squidex/aspnetcore-build-phantomjs-chromium:2.0.3-jessie-fix1 as builder
# 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
COPY src/Squidex/package.json /tmp/package.json COPY src/Squidex/package.json /tmp/package.json
RUN cd /tmp \ RUN cd /tmp \
&& npm install \ && npm install \
&& npm rebuild node-sass && npm rebuild node-sass

1
Squidex.ruleset

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

26
libs/Dockerfile

@ -6,7 +6,7 @@ RUN apt-get update \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release # Install official PhantomJS release
RUN set -x \ RUN set -x \
&& apt-get update \ && apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
@ -23,3 +23,27 @@ RUN set -x \
&& rm -rf /tmp/* /var/lib/apt/lists/* && rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version RUN phantomjs --version
# 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

4
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -8,8 +8,8 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="2.4.6" /> <PackageReference Include="Fody" Version="3.0.3" />
<PackageReference Include="Freezable.Fody" Version="1.8.0" /> <PackageReference Include="Freezable.Fody" Version="1.9.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup> </ItemGroup>

29
src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs

@ -13,6 +13,7 @@ using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
@ -106,7 +107,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
return result; return result;
} }
public static NamedContentData ToApiModel(this NamedContentData content, Schema schema, LanguagesConfig languagesConfig, bool excludeHidden = true) public static NamedContentData ToApiModel(this NamedContentData content, Schema schema, LanguagesConfig languagesConfig, bool excludeHidden = true, bool checkTypeCompatibility = false)
{ {
Guard.NotNull(schema, nameof(schema)); Guard.NotNull(schema, nameof(schema));
Guard.NotNull(languagesConfig, nameof(languagesConfig)); Guard.NotNull(languagesConfig, nameof(languagesConfig));
@ -123,6 +124,32 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
continue; continue;
} }
if (checkTypeCompatibility)
{
var isValid = true;
foreach (var value in fieldValue.Value.Values)
{
try
{
if (!value.IsNull())
{
JsonValueConverter.ConvertValue(field, value);
}
}
catch
{
isValid = false;
break;
}
}
if (!isValid)
{
continue;
}
}
var fieldResult = new ContentFieldData(); var fieldResult = new ContentFieldData();
var fieldValues = fieldValue.Value; var fieldValues = fieldValue.Value;

17
src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleUrlGenerator.cs

@ -0,0 +1,17 @@
// ==========================================================================
// 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 interface IRuleUrlGenerator
{
string GenerateContentUIUrl(NamedId<Guid> appId, NamedId<Guid> schemaId, Guid contentId);
}
}

76
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -5,9 +5,11 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// =========================================-================================= // =========================================-=================================
using System;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -15,6 +17,7 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.HandleRules namespace Squidex.Domain.Apps.Core.HandleRules
{ {
@ -28,14 +31,27 @@ namespace Squidex.Domain.Apps.Core.HandleRules
private const string TimestampDatePlaceholder = "$TIMESTAMP_DATE"; private const string TimestampDatePlaceholder = "$TIMESTAMP_DATE";
private const string TimestampDateTimePlaceholder = "$TIMESTAMP_DATETIME"; private const string TimestampDateTimePlaceholder = "$TIMESTAMP_DATETIME";
private const string ContentActionPlaceholder = "$CONTENT_ACTION"; private const string ContentActionPlaceholder = "$CONTENT_ACTION";
private const string ContentUrlPlaceholder = "$CONTENT_URL";
private const string UserNamePlaceholder = "$USER_NAME";
private const string UserEmailPlaceholder = "$USER_EMAIL";
private static readonly Regex ContentDataPlaceholder = new Regex(@"\$CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled); private static readonly Regex ContentDataPlaceholder = new Regex(@"\$CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled);
private static readonly TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10);
private readonly JsonSerializer serializer; private readonly JsonSerializer serializer;
private readonly IRuleUrlGenerator urlGenerator;
private readonly IMemoryCache memoryCache;
private readonly IUserResolver userResolver;
public RuleEventFormatter(JsonSerializer serializer) public RuleEventFormatter(JsonSerializer serializer, IRuleUrlGenerator urlGenerator, IMemoryCache memoryCache, IUserResolver userResolver)
{ {
Guard.NotNull(memoryCache, nameof(memoryCache));
Guard.NotNull(serializer, nameof(serializer)); Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
Guard.NotNull(userResolver, nameof(userResolver));
this.memoryCache = memoryCache;
this.serializer = serializer; this.serializer = serializer;
this.userResolver = userResolver;
this.urlGenerator = urlGenerator;
} }
public virtual JToken ToRouteData(object value) public virtual JToken ToRouteData(object value)
@ -75,6 +91,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
sb.Replace(SchemaNamePlaceholder, schemaEvent.SchemaId.Name); sb.Replace(SchemaNamePlaceholder, schemaEvent.SchemaId.Name);
} }
if (@event.Payload is ContentEvent contentEvent)
{
sb.Replace(ContentUrlPlaceholder, urlGenerator.GenerateContentUIUrl(@event.Payload.AppId, contentEvent.SchemaId, contentEvent.ContentId));
}
FormatUserInfo(@event, sb);
FormatContentAction(@event, sb); FormatContentAction(@event, sb);
var result = sb.ToString(); var result = sb.ToString();
@ -92,6 +114,39 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return result; return result;
} }
private void FormatUserInfo(Envelope<AppEvent> @event, StringBuilder sb)
{
var text = sb.ToString();
if (text.Contains(UserEmailPlaceholder) || text.Contains(UserNamePlaceholder))
{
var actor = @event.Payload.Actor;
if (actor.Type.Equals("client", StringComparison.OrdinalIgnoreCase))
{
var displayText = actor.ToString();
sb.Replace(UserEmailPlaceholder, displayText);
sb.Replace(UserNamePlaceholder, displayText);
}
else
{
var user = FindUser(actor);
if (user != null)
{
sb.Replace(UserEmailPlaceholder, user.Email);
sb.Replace(UserNamePlaceholder, user.DisplayName());
}
else
{
sb.Replace(UserEmailPlaceholder, Undefined);
sb.Replace(UserNamePlaceholder, Undefined);
}
}
}
}
private static void FormatContentAction(Envelope<AppEvent> @event, StringBuilder sb) private static void FormatContentAction(Envelope<AppEvent> @event, StringBuilder sb)
{ {
switch (@event.Payload) switch (@event.Payload)
@ -166,5 +221,24 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return value?.ToString(Formatting.Indented) ?? Undefined; return value?.ToString(Formatting.Indented) ?? Undefined;
}); });
} }
private IUser FindUser(RefToken actor)
{
var key = $"RuleEventFormatter_Users_${actor.Identifier}";
return memoryCache.GetOrCreate(key, x =>
{
x.AbsoluteExpirationRelativeToNow = UserCacheDuration;
try
{
return userResolver.FindByIdOrEmailAsync(actor.Identifier).Result;
}
catch
{
return null;
}
});
}
} }
} }

9
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -11,20 +11,21 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" /> <ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" />
<ProjectReference Include="..\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" /> <ProjectReference Include="..\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="4.2.2" /> <PackageReference Include="Algolia.Search" Version="4.2.2" />
<PackageReference Include="Elasticsearch.Net" Version="6.0.2" /> <PackageReference Include="Elasticsearch.Net" Version="6.0.2" />
<PackageReference Include="Jint" Version="2.11.58" /> <PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.4.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.4.4" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NJsonSchema" Version="9.10.35" /> <PackageReference Include="NJsonSchema" Version="9.10.35" />
<PackageReference Include="NodaTime" Version="2.2.4" /> <PackageReference Include="NodaTime" Version="2.2.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="WindowsAzure.Storage" Version="9.1.0" /> <PackageReference Include="WindowsAzure.Storage" Version="9.1.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
private JsonValueConverter(JToken value) private JsonValueConverter(JToken value)
{ {
this.Value = value; Value = value;
} }
public static object ConvertValue(Field field, JToken json) public static object ConvertValue(Field field, JToken json)

34
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -26,12 +26,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public partial class MongoContentRepository : MongoRepositoryBase<MongoContentEntity>, IContentRepository public partial class MongoContentRepository : MongoRepositoryBase<MongoContentEntity>, IContentRepository
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IMongoCollection<MongoContentEntity> archiveCollection;
protected IMongoCollection<MongoContentEntity> ArchiveCollection
{
get { return archiveCollection; }
}
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider) public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider)
: base(database) : base(database)
@ -39,8 +33,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider; this.appProvider = appProvider;
archiveCollection = database.GetCollection<MongoContentEntity>("States_Contents_Archive");
} }
protected override string CollectionName() protected override string CollectionName()
@ -52,15 +44,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
await collection.Indexes.TryDropOneAsync("si_1_st_1_dl_1_dt_text"); await collection.Indexes.TryDropOneAsync("si_1_st_1_dl_1_dt_text");
await archiveCollection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.ScheduledTo));
await archiveCollection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.Id)
.Ascending(x => x.Version));
await collection.Indexes.CreateOneAsync( await collection.Indexes.CreateOneAsync(
Index Index
.Text(x => x.DataText) .Text(x => x.DataText)
@ -150,17 +133,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList(); return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList();
} }
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version)
{
var contentEntity =
await ArchiveCollection.Find(x => x.Id == id && x.Version >= version).SortBy(x => x.Version)
.FirstOrDefaultAsync();
contentEntity?.ParseData(schema.SchemaDef);
return contentEntity;
}
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id)
{ {
var contentEntity = var contentEntity =
@ -181,11 +153,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}); });
} }
public override async Task ClearAsync() public Task DeleteArchiveAsync()
{ {
await Database.DropCollectionAsync("States_Contents_Archive"); return Database.DropCollectionAsync("States_Contents_Archive");
await base.ClearAsync();
} }
} }
} }

2
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -89,8 +89,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
document.DocumentId = $"{key}_{newVersion}"; document.DocumentId = $"{key}_{newVersion}";
await ArchiveCollection.ReplaceOneAsync(x => x.DocumentId == document.DocumentId, document, Upsert);
} }
private async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid schemaId) private async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid schemaId)

4
src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -15,8 +15,8 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" /> <ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.4.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.4.4" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" /> <PackageReference Include="MongoDB.Driver" Version="2.5.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />

186
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -17,6 +17,8 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
@ -26,128 +28,180 @@ namespace Squidex.Domain.Apps.Entities
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
private readonly IAppRepository appRepository; private readonly IAppRepository appRepository;
private readonly IRuleRepository ruleRepository; private readonly IRuleRepository ruleRepository;
private readonly IRequestCache requestCache;
private readonly ISchemaRepository schemaRepository; private readonly ISchemaRepository schemaRepository;
public AppProvider( public AppProvider(
IGrainFactory grainFactory, IGrainFactory grainFactory,
IAppRepository appRepository, IAppRepository appRepository,
ISchemaRepository schemaRepository, ISchemaRepository schemaRepository,
IRuleRepository ruleRepository) IRuleRepository ruleRepository,
IRequestCache requestCache)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(appRepository, nameof(appRepository)); Guard.NotNull(appRepository, nameof(appRepository));
Guard.NotNull(schemaRepository, nameof(schemaRepository)); Guard.NotNull(schemaRepository, nameof(schemaRepository));
Guard.NotNull(requestCache, nameof(requestCache));
Guard.NotNull(ruleRepository, nameof(ruleRepository)); Guard.NotNull(ruleRepository, nameof(ruleRepository));
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
this.appRepository = appRepository; this.appRepository = appRepository;
this.schemaRepository = schemaRepository; this.schemaRepository = schemaRepository;
this.requestCache = requestCache;
this.ruleRepository = ruleRepository; this.ruleRepository = ruleRepository;
} }
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
{ {
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync(); return requestCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () =>
if (!IsExisting(app))
{ {
return (null, null); using (Profile.Method<AppProvider>())
} {
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync(); if (!IsExisting(app))
{
return (null, null);
}
if (!IsExisting(schema, false)) var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
{
return (null, null);
}
return (app.Value, schema.Value); if (!IsExisting(schema, false))
{
return (null, null);
}
return (app.Value, schema.Value);
}
});
} }
public async Task<IAppEntity> GetAppAsync(string appName) public Task<IAppEntity> GetAppAsync(string appName)
{ {
var appId = await GetAppIdAsync(appName); return requestCache.GetOrCreateAsync($"GetAppAsync({appName})", async () =>
if (appId == Guid.Empty)
{ {
return null; using (Profile.Method<AppProvider>())
} {
var appId = await GetAppIdAsync(appName);
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync(); if (appId == Guid.Empty)
{
return null;
}
if (!IsExisting(app)) var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
{
return null; if (!IsExisting(app))
} {
return null;
}
return app.Value; return app.Value;
}
});
} }
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name) public Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name)
{ {
var schemaId = await GetSchemaIdAsync(appId, name); return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () =>
if (schemaId == Guid.Empty)
{ {
return null; using (Profile.Method<AppProvider>())
} {
var schemaId = await GetSchemaIdAsync(appId, name);
return await GetSchemaAsync(appId, schemaId, false);
if (schemaId == Guid.Empty)
{
return null;
}
return await GetSchemaAsync(appId, schemaId, false);
}
});
} }
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) public Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{ {
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync(); return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () =>
if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId)
{ {
return null; using (Profile.Method<AppProvider>())
} {
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
return schema.Value;
if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId)
{
return null;
}
return schema.Value;
}
});
} }
public async Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId) public Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId)
{ {
var ids = await schemaRepository.QuerySchemaIdsAsync(appId); return requestCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () =>
{
using (Profile.Method<AppProvider>())
{
var ids = await schemaRepository.QuerySchemaIdsAsync(appId);
var schemas = var schemas =
await Task.WhenAll( await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync())); ids.Select(id => grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync()));
return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList(); return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList();
}
});
} }
public async Task<List<IRuleEntity>> GetRulesAsync(Guid appId) public Task<List<IRuleEntity>> GetRulesAsync(Guid appId)
{ {
var ids = await ruleRepository.QueryRuleIdsAsync(appId); return requestCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () =>
{
using (Profile.Method<AppProvider>())
{
var ids = await ruleRepository.QueryRuleIdsAsync(appId);
var rules = var rules =
await Task.WhenAll( await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync())); ids.Select(id => grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync()));
return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList(); return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList();
}
});
} }
public async Task<List<IAppEntity>> GetUserApps(string userId) public Task<List<IAppEntity>> GetUserApps(string userId)
{ {
var ids = await appRepository.QueryUserAppIdsAsync(userId); return requestCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{
using (Profile.Method<AppProvider>())
{
var ids = await appRepository.QueryUserAppIdsAsync(userId);
var apps = var apps =
await Task.WhenAll( await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync())); ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList(); return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList();
}
});
} }
private Task<Guid> GetAppIdAsync(string name) private async Task<Guid> GetAppIdAsync(string name)
{ {
return appRepository.FindAppIdByNameAsync(name); using (Profile.Method<AppProvider>())
{
return await appRepository.FindAppIdByNameAsync(name);
}
} }
private async Task<Guid> GetSchemaIdAsync(Guid appId, string name) private async Task<Guid> GetSchemaIdAsync(Guid appId, string name)
{ {
return await schemaRepository.FindSchemaIdAsync(appId, name); using (Profile.Method<AppProvider>())
{
return await schemaRepository.FindSchemaIdAsync(appId, name);
}
} }
private static bool IsFound(IEntityWithVersion entity) private static bool IsFound(IEntityWithVersion entity)
@ -155,14 +209,14 @@ namespace Squidex.Domain.Apps.Entities
return entity.Version > EtagVersion.Empty; return entity.Version > EtagVersion.Empty;
} }
private static bool IsExisting(J<ISchemaEntity> schema, bool allowDeleted) private static bool IsExisting(J<IAppEntity> app)
{ {
return IsFound(schema.Value) && (!schema.Value.IsDeleted || allowDeleted); return IsFound(app.Value) && !app.Value.IsArchived;
} }
private static bool IsExisting(J<IAppEntity> app) private static bool IsExisting(J<ISchemaEntity> schema, bool allowDeleted)
{ {
return IsFound(app.Value) && !app.Value.IsArchived; return IsFound(schema.Value) && (!schema.Value.IsDeleted || allowDeleted);
} }
} }
} }

8
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -36,11 +37,12 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppGrain( public AppGrain(
InitialPatterns initialPatterns, InitialPatterns initialPatterns,
IStore<Guid> store, IStore<Guid> store,
ISemanticLog log,
IAppProvider appProvider, IAppProvider appProvider,
IAppPlansProvider appPlansProvider, IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager, IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver) IUserResolver userResolver)
: base(store) : base(store, log)
{ {
Guard.NotNull(initialPatterns, nameof(initialPatterns)); Guard.NotNull(initialPatterns, nameof(initialPatterns));
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
@ -70,11 +72,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
}); });
case AssignContributor assigneContributor: case AssignContributor assigneContributor:
return UpdateAsync(assigneContributor, async c => return UpdateReturnAsync(assigneContributor, async c =>
{ {
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId)); await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
AssignContributor(c); AssignContributor(c);
return EntityCreatedResult.Create(c.ContributorId, NewVersion);
}); });
case RemoveContributor removeContributor: case RemoveContributor removeContributor:

28
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
@ -34,20 +35,31 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
} }
else else
{ {
if (await users.FindByIdAsync(command.ContributorId) == null) var user = await users.FindByIdOrEmailAsync(command.ContributorId);
if (user == null)
{ {
error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId))); error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId)));
} }
else if (contributors.TryGetValue(command.ContributorId, out var existing)) else
{ {
if (existing == command.Permission) command.ContributorId = user.Id;
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase))
{ {
error(new ValidationError("Contributor has already this permission.", nameof(command.Permission))); error(new ValidationError("You cannot change your own permission."));
}
else if (contributors.TryGetValue(command.ContributorId, out var existing))
{
if (existing == command.Permission)
{
error(new ValidationError("Contributor has already this permission.", nameof(command.Permission)));
}
}
else if (plan.MaxContributors == contributors.Count)
{
error(new ValidationError("You have reached the maximum number of contributors for your plan."));
} }
}
else if (plan.MaxContributors == contributors.Count)
{
error(new ValidationError("You have reached the maximum number of contributors for your plan."));
} }
} }
}); });

4
src/Squidex.Domain.Apps.Entities/Apps/Services/IAppLimitsPlan.cs

@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services
string Costs { get; } string Costs { get; }
string YearlyCosts { get; }
string YearlyId { get; }
long MaxApiCalls { get; } long MaxApiCalls { get; }
long MaxAssetSize { get; } long MaxAssetSize { get; }

4
src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppLimitsPlan.cs

@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations
public string Costs { get; set; } public string Costs { get; set; }
public string YearlyCosts { get; set; }
public string YearlyId { get; set; }
public long MaxApiCalls { get; set; } public long MaxApiCalls { get; set; }
public long MaxAssetSize { get; set; } public long MaxAssetSize { get; set; }

16
src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/ConfigAppPlansProvider.cs

@ -23,15 +23,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations
MaxContributors = -1 MaxContributors = -1
}; };
private readonly Dictionary<string, ConfigAppLimitsPlan> plansById; private readonly Dictionary<string, ConfigAppLimitsPlan> plansById = new Dictionary<string, ConfigAppLimitsPlan>(StringComparer.OrdinalIgnoreCase);
private readonly List<ConfigAppLimitsPlan> plansList; private readonly List<ConfigAppLimitsPlan> plansList = new List<ConfigAppLimitsPlan>();
public ConfigAppPlansProvider(IEnumerable<ConfigAppLimitsPlan> config) public ConfigAppPlansProvider(IEnumerable<ConfigAppLimitsPlan> config)
{ {
Guard.NotNull(config, nameof(config)); Guard.NotNull(config, nameof(config));
plansList = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList(); foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone()))
plansById = plansList.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); {
plansList.Add(plan);
plansById[plan.Id] = plan;
if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts))
{
plansById[plan.YearlyId] = plan;
}
}
} }
public IEnumerable<IAppLimitsPlan> GetAvailablePlans() public IEnumerable<IAppLimitsPlan> GetAvailablePlans()

5
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -23,8 +24,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public class AssetGrain : SquidexDomainObjectGrain<AssetState>, IAssetGrain public class AssetGrain : SquidexDomainObjectGrain<AssetState>, IAssetGrain
{ {
public AssetGrain(IStore<Guid> store) public AssetGrain(IStore<Guid> store, ISemanticLog log)
: base(store) : base(store, log)
{ {
} }

4
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -33,11 +34,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
public ContentGrain( public ContentGrain(
IStore<Guid> store, IStore<Guid> store,
ISemanticLog log,
IAppProvider appProvider, IAppProvider appProvider,
IAssetRepository assetRepository, IAssetRepository assetRepository,
IScriptEngine scriptEngine, IScriptEngine scriptEngine,
IContentRepository contentRepository) IContentRepository contentRepository)
: base(store) : base(store, log)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(scriptEngine, nameof(scriptEngine));

109
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -13,6 +13,7 @@ using System.Threading.Tasks;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Edm; using Squidex.Domain.Apps.Entities.Contents.Edm;
@ -27,113 +28,130 @@ namespace Squidex.Domain.Apps.Entities.Contents
public sealed class ContentQueryService : IContentQueryService public sealed class ContentQueryService : IContentQueryService
{ {
private readonly IContentRepository contentRepository; private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly EdmModelBuilder modelBuilder; private readonly EdmModelBuilder modelBuilder;
public ContentQueryService( public ContentQueryService(
IContentRepository contentRepository, IContentRepository contentRepository,
IContentVersionLoader contentVersionLoader,
IAppProvider appProvider, IAppProvider appProvider,
IScriptEngine scriptEngine, IScriptEngine scriptEngine,
EdmModelBuilder modelBuilder) EdmModelBuilder modelBuilder)
{ {
Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader));
Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(modelBuilder, nameof(modelBuilder)); Guard.NotNull(modelBuilder, nameof(modelBuilder));
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
this.contentRepository = contentRepository; this.contentRepository = contentRepository;
this.contentVersionLoader = contentVersionLoader;
this.appProvider = appProvider; this.appProvider = appProvider;
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
this.modelBuilder = modelBuilder; this.modelBuilder = modelBuilder;
} }
public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1) public Task ThrowIfSchemaNotExistsAsync(IAppEntity app, string schemaIdOrName)
{
return GetSchemaAsync(app, schemaIdOrName);
}
public async Task<IContentEntity> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1)
{ {
Guard.NotNull(app, nameof(app)); Guard.NotNull(app, nameof(app));
Guard.NotNull(user, nameof(user)); Guard.NotNull(user, nameof(user));
Guard.NotEmpty(id, nameof(id));
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName));
var isFrontendClient = user.IsInClient("squidex-frontend"); var schema = await GetSchemaAsync(app, schemaIdOrName);
var schema = await FindSchemaAsync(app, schemaIdOrName); var isFrontendClient = IsFrontendClient(user);
var isVersioned = version > EtagVersion.Empty;
var content = var content =
version > EtagVersion.Empty ? isVersioned ?
await contentRepository.FindContentAsync(app, schema, id, version) : await FindContentByVersionAsync(id, version) :
await contentRepository.FindContentAsync(app, schema, id); await FindContentAsync(app, id, schema);
if (content == null || (content.Status != Status.Published && !isFrontendClient)) if (content == null || (content.Status != Status.Published && !isFrontendClient) || content.SchemaId.Id != schema.Id)
{ {
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity));
} }
content = TransformContent(user, schema, Enumerable.Repeat(content, 1)).FirstOrDefault(); content = TransformContent(app, schema, user, Enumerable.Repeat(content, 1), isVersioned, isFrontendClient).FirstOrDefault();
return (schema, content); return content;
} }
public async Task<(ISchemaEntity Schema, IResultList<IContentEntity> Contents)> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query)
{ {
Guard.NotNull(app, nameof(app)); Guard.NotNull(app, nameof(app));
Guard.NotNull(user, nameof(user)); Guard.NotNull(user, nameof(user));
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName));
var schema = await FindSchemaAsync(app, schemaIdOrName); var schema = await GetSchemaAsync(app, schemaIdOrName);
var isFrontendClient = IsFrontendClient(user);
var parsedQuery = ParseQuery(app, query, schema); var parsedQuery = ParseQuery(app, query, schema);
var parsedStatus = ParseStatus(user, archived); var parsedStatus = ParseStatus(isFrontendClient, archived);
var contents = await contentRepository.QueryAsync(app, schema, parsedStatus.ToArray(), parsedQuery); var contents = await contentRepository.QueryAsync(app, schema, parsedStatus.ToArray(), parsedQuery);
return TransformContents(user, schema, contents); return TransformContents(app, schema, user, contents, false, isFrontendClient);
} }
public async Task<(ISchemaEntity Schema, IResultList<IContentEntity> Contents)> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids)
{ {
Guard.NotNull(ids, nameof(ids)); Guard.NotNull(ids, nameof(ids));
Guard.NotNull(app, nameof(app)); Guard.NotNull(app, nameof(app));
Guard.NotNull(user, nameof(user)); Guard.NotNull(user, nameof(user));
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName));
var schema = await FindSchemaAsync(app, schemaIdOrName); var schema = await GetSchemaAsync(app, schemaIdOrName);
var isFrontendClient = IsFrontendClient(user);
var parsedStatus = ParseStatus(user, archived); var parsedStatus = ParseStatus(isFrontendClient, archived);
var contents = await contentRepository.QueryAsync(app, schema, parsedStatus.ToArray(), ids); var contents = await contentRepository.QueryAsync(app, schema, parsedStatus.ToArray(), ids);
return TransformContents(user, schema, contents); return TransformContents(app, schema, user, contents, false, isFrontendClient);
} }
private (ISchemaEntity Schema, IResultList<IContentEntity> Contents) TransformContents(ClaimsPrincipal user, ISchemaEntity schema, IResultList<IContentEntity> contents) private IResultList<IContentEntity> TransformContents(IAppEntity app, ISchemaEntity schema, ClaimsPrincipal user,
IResultList<IContentEntity> contents,
bool isTypeChecking,
bool isFrontendClient)
{ {
var transformed = TransformContent(user, schema, contents); var transformed = TransformContent(app, schema, user, contents, isTypeChecking, isFrontendClient);
return (schema, ResultList.Create(transformed, contents.Total)); return ResultList.Create(transformed, contents.Total);
} }
private IEnumerable<IContentEntity> TransformContent(ClaimsPrincipal user, ISchemaEntity schema, IEnumerable<IContentEntity> contents) private IEnumerable<IContentEntity> TransformContent(IAppEntity app, ISchemaEntity schema, ClaimsPrincipal user,
IEnumerable<IContentEntity> contents,
bool isTypeChecking,
bool isFrontendClient)
{ {
var scriptText = schema.ScriptQuery; var scriptText = schema.ScriptQuery;
if (!string.IsNullOrWhiteSpace(scriptText)) var isScripting = !string.IsNullOrWhiteSpace(scriptText);
{
foreach (var content in contents)
{
var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText);
var contentResult = SimpleMapper.Map(content, new ContentEntity());
contentResult.Data = contentData;
yield return contentResult; foreach (var content in contents)
}
}
else
{ {
foreach (var content in contents) var result = SimpleMapper.Map(content, new ContentEntity());
if (!isFrontendClient && isScripting)
{ {
yield return content; result.Data = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText);
} }
result.Data = result.Data.ToApiModel(schema.SchemaDef, app.LanguagesConfig, isFrontendClient, isTypeChecking);
yield return result;
} }
} }
@ -151,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
public async Task<ISchemaEntity> FindSchemaAsync(IAppEntity app, string schemaIdOrName) public async Task<ISchemaEntity> GetSchemaAsync(IAppEntity app, string schemaIdOrName)
{ {
Guard.NotNull(app, nameof(app)); Guard.NotNull(app, nameof(app));
@ -175,11 +193,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
return schema; return schema;
} }
private static List<Status> ParseStatus(ClaimsPrincipal user, bool archived) private static List<Status> ParseStatus(bool isFrontendClient, bool archived)
{ {
var status = new List<Status>(); var status = new List<Status>();
if (user.IsInClient("squidex-frontend")) if (isFrontendClient)
{ {
if (archived) if (archived)
{ {
@ -198,5 +216,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
return status; return status;
} }
private Task<IContentEntity> FindContentByVersionAsync(Guid id, long version)
{
return contentVersionLoader.LoadAsync(id, version);
}
private Task<IContentEntity> FindContentAsync(IAppEntity app, Guid id, ISchemaEntity schema)
{
return contentRepository.FindContentAsync(app, schema, id);
}
private static bool IsFrontendClient(ClaimsPrincipal user)
{
return user.IsInClient("squidex-frontend");
}
} }
} }

55
src/Squidex.Domain.Apps.Entities/Contents/ContentVersionLoader.cs

@ -0,0 +1,55 @@
// ==========================================================================
// 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.Schemas;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentVersionLoader : IContentVersionLoader
{
private readonly IStore<Guid> store;
private readonly FieldRegistry registry;
public ContentVersionLoader(IStore<Guid> store, FieldRegistry registry)
{
Guard.NotNull(store, nameof(store));
Guard.NotNull(registry, nameof(registry));
this.store = store;
this.registry = registry;
}
public async Task<IContentEntity> LoadAsync(Guid id, long version)
{
var content = new ContentState();
var persistence = store.WithEventSourcing<ContentGrain, Guid>(id, e =>
{
if (content.Version < version)
{
content = content.Apply(e);
content.Version++;
}
});
await persistence.ReadAsync();
if (content.Version != version)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity));
}
return content;
}
}
}

5
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs

@ -7,6 +7,7 @@
using System; using System;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
@ -26,6 +27,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType Boolean = new BooleanGraphType(); public static readonly IGraphType Boolean = new BooleanGraphType();
public static readonly IGraphType StatusType = new EnumerationGraphType<Status>();
public static readonly IGraphType NonNullInt = new NonNullGraphType(new IntGraphType()); public static readonly IGraphType NonNullInt = new NonNullGraphType(new IntGraphType());
public static readonly IGraphType NonNullGuid = new NonNullGraphType(new GuidGraphType()); public static readonly IGraphType NonNullGuid = new NonNullGraphType(new GuidGraphType());
@ -38,6 +41,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType NonNullBoolean = new NonNullGraphType(new BooleanGraphType()); public static readonly IGraphType NonNullBoolean = new NonNullGraphType(new BooleanGraphType());
public static readonly IGraphType NonNullStatusType = new NonNullGraphType(new EnumerationGraphType<Status>());
public static readonly IGraphType ListOfNonNullGuid = new ListGraphType(new NonNullGraphType(new GuidGraphType())); public static readonly IGraphType ListOfNonNullGuid = new ListGraphType(new NonNullGraphType(new GuidGraphType()));
public static readonly IGraphType ListOfNonNullString = new ListGraphType(new NonNullGraphType(new StringGraphType())); public static readonly IGraphType ListOfNonNullString = new ListGraphType(new NonNullGraphType(new StringGraphType()));

8
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs

@ -70,6 +70,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The user that has updated the {schemaName} content last." Description = $"The user that has updated the {schemaName} content last."
}); });
AddField(new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullStatusType,
Resolver = Resolve(x => x.Status),
Description = $"The the status of the {schemaName} content."
});
AddField(new FieldType AddField(new FieldType
{ {
Name = "url", Name = "url",

9
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -10,19 +10,18 @@ using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentQueryService public interface IContentQueryService
{ {
Task<(ISchemaEntity Schema, IResultList<IContentEntity> Contents)> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids);
Task<(ISchemaEntity Schema, IResultList<IContentEntity> Contents)> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query);
Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = EtagVersion.Any); Task<IContentEntity> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = EtagVersion.Any);
Task<ISchemaEntity> FindSchemaAsync(IAppEntity app, string schemaIdOrName); Task ThrowIfSchemaNotExistsAsync(IAppEntity app, string schemaIdOrName);
} }
} }

17
src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentVersionLoader
{
Task<IContentEntity> LoadAsync(Guid id, long version);
}
}

8
src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs

@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (content == null) if (content == null)
{ {
content = (await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id)).Content; content = await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id);
if (content != null) if (content != null)
{ {
@ -96,12 +96,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var result = await contentQuery.QueryAsync(app, schemaIdOrName, user, false, query); var result = await contentQuery.QueryAsync(app, schemaIdOrName, user, false, query);
foreach (var content in result.Contents) foreach (var content in result)
{ {
cachedContents[content.Id] = content; cachedContents[content.Id] = content;
} }
return result.Contents; return result;
} }
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids) public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids)
@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var result = await contentQuery.QueryAsync(app, schemaId.ToString(), user, false, notLoadedContents); var result = await contentQuery.QueryAsync(app, schemaId.ToString(), user, false, notLoadedContents);
foreach (var content in result.Contents) foreach (var content in result)
{ {
cachedContents[content.Id] = content; cachedContents[content.Id] = content;
} }

2
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -27,8 +27,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version);
Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback); Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback);
} }
} }

2
src/Squidex.Domain.Apps.Entities/DomainObjectState.cs

@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
[JsonProperty] [JsonProperty]
public long Version { get; set; } public long Version { get; set; } = EtagVersion.Empty;
public T Clone() public T Clone()
{ {

5
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -25,8 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Rules
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
public RuleGrain(IStore<Guid> store, IAppProvider appProvider) public RuleGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider)
: base(store) : base(store, log)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));

5
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
@ -29,8 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly FieldRegistry registry; private readonly FieldRegistry registry;
public SchemaGrain(IStore<Guid> store, IAppProvider appProvider, FieldRegistry registry) public SchemaGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, FieldRegistry registry)
: base(store) : base(store, log)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(registry, nameof(registry)); Guard.NotNull(registry, nameof(registry));

5
src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -14,8 +14,9 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0-rc2" /> <PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0" />
<PackageReference Include="NodaTime" Version="2.2.4" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.0.0" />
<PackageReference Include="NodaTime" Version="2.2.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />

5
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs

@ -9,14 +9,15 @@ using System;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public abstract class SquidexDomainObjectGrain<T> : DomainObjectGrain<T> where T : IDomainState, new() public abstract class SquidexDomainObjectGrain<T> : DomainObjectGrain<T> where T : IDomainState, new()
{ {
protected SquidexDomainObjectGrain(IStore<Guid> store) protected SquidexDomainObjectGrain(IStore<Guid> store, ISemanticLog log)
: base(store) : base(store, log)
{ {
} }

2
src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -13,7 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="GraphQL" Version="0.17.3" /> <PackageReference Include="GraphQL" Version="0.17.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.0.1" />
<PackageReference Include="NodaTime" Version="2.2.4" /> <PackageReference Include="NodaTime" Version="2.2.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.8.0" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.8.0" />

32
src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -12,6 +12,7 @@ using System.Security.Claims;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
@ -72,11 +73,6 @@ namespace Squidex.Domain.Users.MongoDb
return new MongoUser { Email = email, UserName = email }; return new MongoUser { Email = email, UserName = email };
} }
public async Task<IUser> FindByIdAsync(string id)
{
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken) public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{ {
return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken);
@ -388,5 +384,31 @@ namespace Squidex.Domain.Users.MongoDb
return TaskHelper.Done; return TaskHelper.Done;
} }
public async Task<IUser> FindByIdOrEmailAsync(string id)
{
if (ObjectId.TryParse(id, out var parsed))
{
return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
else
{
return await Collection.Find(x => x.NormalizedEmail == id.ToUpperInvariant()).FirstOrDefaultAsync();
}
}
public Task<List<IUser>> QueryByEmailAsync(string email)
{
var result = Users;
if (!string.IsNullOrWhiteSpace(email))
{
var normalizedEmail = email.ToUpperInvariant();
result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail));
}
return Task.FromResult(result.Select(x => x).ToList());
}
} }
} }

4
src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -13,10 +13,10 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IdentityServer4" Version="2.1.2" /> <PackageReference Include="IdentityServer4" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.0.2" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.4.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.4.0" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" /> <PackageReference Include="MongoDB.Driver" Version="2.5.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.4.1" /> <PackageReference Include="System.Security.Principal.Windows" Version="4.4.1" />

3
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -71,8 +71,9 @@ namespace Squidex.Domain.Users
return user; return user;
} }
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName) public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName, bool hidden)
{ {
user.SetHidden(hidden);
user.SetEmail(email); user.SetEmail(email);
user.SetDisplayName(displayName); user.SetDisplayName(displayName);

2
src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj

@ -6,7 +6,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="WindowsAzure.Storage" Version="9.1.0" /> <PackageReference Include="WindowsAzure.Storage" Version="9.1.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />

5
src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs

@ -30,12 +30,13 @@ namespace Squidex.Infrastructure.EventSourcing
Guard.NotNull(subscriber, nameof(subscriber)); Guard.NotNull(subscriber, nameof(subscriber));
this.connection = connection; this.connection = connection;
this.position = projectionClient.ParsePositionOrNull(position); this.position = projectionClient.ParsePositionOrNull(position);
this.subscriber = subscriber;
var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result; var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result;
subscription = SubscribeToStream(streamName); this.subscriber = subscriber;
this.subscription = SubscribeToStream(streamName);
} }
public Task StopAsync() public Task StopAsync()

4
src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -11,8 +11,8 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.4.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.4.4" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" /> <PackageReference Include="MongoDB.Driver" Version="2.5.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.8.0" /> <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.8.0" />

68
src/Squidex.Infrastructure/Caching/HttpRequestCache.cs

@ -0,0 +1,68 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
namespace Squidex.Infrastructure.Caching
{
public sealed class HttpRequestCache : IRequestCache
{
private readonly IHttpContextAccessor httpContextAccessor;
public HttpRequestCache(IHttpContextAccessor httpContextAccessor)
{
Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor));
this.httpContextAccessor = httpContextAccessor;
}
public void Add(object key, object value)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
items[cacheKey] = value;
}
}
public void Remove(object key)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
items?.Remove(cacheKey);
}
}
public bool TryGetValue(object key, out object value)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
return items.TryGetValue(cacheKey, out value);
}
value = null;
return false;
}
private static string GetCacheKey(object key)
{
return $"CACHE_{key}";
}
}
}

18
src/Squidex.Infrastructure/Caching/IRequestCache.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Caching
{
public interface IRequestCache
{
void Add(object key, object value);
void Remove(object key);
bool TryGetValue(object key, out object value);
}
}

43
src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs

@ -0,0 +1,43 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Caching
{
public static class RequestCacheExtensions
{
public static async Task<T> GetOrCreateAsync<T>(this IRequestCache cache, object key, Func<Task<T>> task)
{
if (cache.TryGetValue(key, out var value) && value is T typedValue)
{
return typedValue;
}
typedValue = await task();
cache.Add(key, typedValue);
return typedValue;
}
public static T GetOrCreate<T>(this IRequestCache cache, object key, Func<T> task)
{
if (cache.TryGetValue(key, out var value) && value is T typedValue)
{
return typedValue;
}
typedValue = task();
cache.Add(key, typedValue);
return typedValue;
}
}
}

2
src/Squidex.Domain.Apps.Entities/CachingProviderBase.cs → src/Squidex.Infrastructure/CachingProviderBase.cs

@ -8,7 +8,7 @@
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Infrastructure
{ {
public abstract class CachingProviderBase public abstract class CachingProviderBase
{ {

24
src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
@ -19,6 +20,7 @@ namespace Squidex.Infrastructure.Commands
{ {
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>(); private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private readonly IStore<Guid> store; private readonly IStore<Guid> store;
private readonly ISemanticLog log;
private Guid id; private Guid id;
private T snapshot = new T { Version = EtagVersion.Empty }; private T snapshot = new T { Version = EtagVersion.Empty };
private IPersistence<T> persistence; private IPersistence<T> persistence;
@ -43,20 +45,29 @@ namespace Squidex.Infrastructure.Commands
get { return snapshot; } get { return snapshot; }
} }
protected DomainObjectGrain(IStore<Guid> store) protected DomainObjectGrain(IStore<Guid> store, ISemanticLog log)
{ {
Guard.NotNull(store, nameof(store)); Guard.NotNull(store, nameof(store));
Guard.NotNull(log, nameof(log));
this.store = store; this.store = store;
this.log = log;
} }
public override Task OnActivateAsync(Guid key) public override async Task OnActivateAsync(Guid key)
{ {
id = key; using (log.MeasureInformation(w => w
.WriteProperty("action", "ActivateDomainObject")
.WriteProperty("domainObjectType", GetType().Name)
.WriteProperty("domainObjectKey", key.ToString())))
{
id = key;
persistence = store.WithSnapshotsAndEventSourcing<T, Guid>(GetType(), id, ApplySnapshot, ApplyEvent); persistence = store.WithSnapshotsAndEventSourcing<T, Guid>(GetType(), id, ApplySnapshot, ApplyEvent);
return persistence.ReadAsync(); await persistence.ReadAsync();
}
} }
public void RaiseEvent(IEvent @event) public void RaiseEvent(IEvent @event)
@ -162,7 +173,8 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectNotFoundException(id.ToString(), GetType()); throw new DomainObjectNotFoundException(id.ToString(), GetType());
} }
else if (!isUpdate && Version >= 0)
if (!isUpdate && Version >= 0)
{ {
throw new DomainException("Object has already been created."); throw new DomainException("Object has already been created.");
} }

6
src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs

@ -14,12 +14,12 @@ namespace Squidex.Infrastructure.Commands
{ {
public static string Format(IGrainCallContext context) public static string Format(IGrainCallContext context)
{ {
if (context.Method == null) if (context.InterfaceMethod == null)
{ {
return "Unknown"; return "Unknown";
} }
if (string.Equals(context.Method.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) && if (string.Equals(context.InterfaceMethod.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) &&
context.Arguments?.Length == 1 && context.Arguments?.Length == 1 &&
context.Arguments[0] != null) context.Arguments[0] != null)
{ {
@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.Commands
return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})"; return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})";
} }
return context.Method.Name; return context.InterfaceMethod.Name;
} }
} }
} }

2
src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs

@ -63,7 +63,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
public Task<Immutable<EventConsumerInfo>> GetStateAsync() public Task<Immutable<EventConsumerInfo>> GetStateAsync()
{ {
return Task.FromResult(state.ToInfo(this.eventConsumer.Name).AsImmutable()); return Task.FromResult(state.ToInfo(eventConsumer.Name).AsImmutable());
} }
public Task OnEventAsync(Immutable<IEventSubscription> subscription, Immutable<StoredEvent> storedEvent) public Task OnEventAsync(Immutable<IEventSubscription> subscription, Immutable<StoredEvent> storedEvent)

17
src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs

@ -10,26 +10,23 @@ using Orleans;
namespace Squidex.Infrastructure.EventSourcing.Grains namespace Squidex.Infrastructure.EventSourcing.Grains
{ {
public sealed class OrleansEventNotifier : IEventNotifier, IInitializable public sealed class OrleansEventNotifier : IEventNotifier
{ {
private readonly IGrainFactory factory; private readonly Lazy<IEventConsumerManagerGrain> eventConsumerManagerGrain;
private IEventConsumerManagerGrain eventConsumerManagerGrain;
public OrleansEventNotifier(IGrainFactory factory) public OrleansEventNotifier(IGrainFactory factory)
{ {
Guard.NotNull(factory, nameof(factory)); Guard.NotNull(factory, nameof(factory));
this.factory = factory; eventConsumerManagerGrain = new Lazy<IEventConsumerManagerGrain>(() =>
} {
return factory.GetGrain<IEventConsumerManagerGrain>("Default");
public void Initialize() });
{
eventConsumerManagerGrain = factory.GetGrain<IEventConsumerManagerGrain>("Default");
} }
public void NotifyEventsStored(string streamName) public void NotifyEventsStored(string streamName)
{ {
eventConsumerManagerGrain?.ActivateAsync(streamName); eventConsumerManagerGrain.Value.ActivateAsync(streamName);
} }
public IDisposable Subscribe(Action<string> handler) public IDisposable Subscribe(Action<string> handler)

3
src/Squidex.Infrastructure/FileExtensions.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Globalization;
using System.IO; using System.IO;
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
@ -62,7 +63,7 @@ namespace Squidex.Infrastructure
u = Extensions.Length - 1; u = Extensions.Length - 1;
} }
return $"{Math.Round(d, 1)} {Extensions[u]}"; return $"{Math.Round(d, 1).ToString(CultureInfo.InvariantCulture)} {Extensions[u]}";
} }
} }
} }

26
src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Squidex.Infrastructure.Log.Adapter namespace Squidex.Infrastructure.Log.Adapter
@ -70,6 +71,22 @@ namespace Squidex.Infrastructure.Log.Adapter
}); });
} }
if (state is IReadOnlyList<KeyValuePair<string, object>> parameters)
{
foreach (var kvp in parameters)
{
if (kvp.Value != null)
{
var key = kvp.Key.Trim('{', '}', ' ');
if (key.Length > 2 && !string.Equals(key, "originalFormat", StringComparison.OrdinalIgnoreCase))
{
writer.WriteProperty(key.ToCamelCase(), kvp.Value.ToString());
}
}
}
}
if (exception != null) if (exception != null)
{ {
writer.WriteException(exception); writer.WriteException(exception);
@ -86,14 +103,5 @@ namespace Squidex.Infrastructure.Log.Adapter
{ {
return NoopDisposable.Instance; return NoopDisposable.Instance;
} }
private class NoopDisposable : IDisposable
{
public static readonly NoopDisposable Instance = new NoopDisposable();
public void Dispose()
{
}
}
} }
} }

14
src/Squidex.Infrastructure/Log/ILogProfilerSessionProvider.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Log
{
public interface ILogProfilerSessionProvider
{
ProfilerSession GetSession();
}
}

24
src/Squidex.Infrastructure/Log/NoopDisposable.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Log
{
public sealed class NoopDisposable : IDisposable
{
public static readonly NoopDisposable Instance = new NoopDisposable();
private NoopDisposable()
{
}
public void Dispose()
{
}
}
}

67
src/Squidex.Infrastructure/Log/Profile.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.Diagnostics;
using System.Runtime.CompilerServices;
namespace Squidex.Infrastructure.Log
{
public static class Profile
{
private static ILogProfilerSessionProvider sessionProvider;
private sealed class Timer : IDisposable
{
private readonly Stopwatch watch = Stopwatch.StartNew();
private readonly ProfilerSession session;
private readonly string key;
public Timer(ProfilerSession session, string key)
{
this.session = session;
this.key = key;
}
public void Dispose()
{
watch.Stop();
session.Measured(key, watch.ElapsedMilliseconds);
}
}
public static void Init(ILogProfilerSessionProvider provider)
{
sessionProvider = provider;
}
public static IDisposable Method<T>([CallerMemberName] string memberName = null)
{
return Key($"{typeof(T).Name}/{memberName}");
}
public static IDisposable Method(string objectName, [CallerMemberName] string memberName = null)
{
return Key($"{objectName}/{memberName}");
}
public static IDisposable Key(string key)
{
Guard.NotNull(key, nameof(key));
var session = sessionProvider?.GetSession();
if (session == null)
{
return NoopDisposable.Instance;
}
return new Timer(session, key);
}
}
}

58
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<string, ProfilerItem> traces = new ConcurrentDictionary<string, ProfilerItem>();
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, k => k
.WriteProperty("elapsedMsTotal", kvp.Value.Total)
.WriteProperty("elapsedMsAvg", kvp.Value.Total / kvp.Value.Count)
.WriteProperty("count", kvp.Value.Count));
}
});
}
}
}
}

22
src/Squidex.Infrastructure/Orleans/Bootstrap.cs

@ -14,6 +14,7 @@ namespace Squidex.Infrastructure.Orleans
{ {
public sealed class Bootstrap<T> : IStartupTask where T : IBackgroundGrain public sealed class Bootstrap<T> : IStartupTask where T : IBackgroundGrain
{ {
private const int NumTries = 10;
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
public Bootstrap(IGrainFactory grainFactory) public Bootstrap(IGrainFactory grainFactory)
@ -23,11 +24,26 @@ namespace Squidex.Infrastructure.Orleans
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
} }
public Task Execute(CancellationToken cancellationToken) public async Task Execute(CancellationToken cancellationToken)
{ {
var grain = grainFactory.GetGrain<T>("Default"); for (var i = 1; i <= NumTries; i++)
{
try
{
var grain = grainFactory.GetGrain<T>("Default");
return grain.ActivateAsync(); await grain.ActivateAsync();
return;
}
catch (OrleansException)
{
if (i == NumTries)
{
throw;
}
}
}
} }
} }
} }

50
src/Squidex.Infrastructure/Orleans/J{T}.cs

@ -11,27 +11,23 @@ using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Orleans.CodeGeneration; using Orleans.CodeGeneration;
using Orleans.Serialization; using Orleans.Serialization;
using Squidex.Infrastructure.Log;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans
{ {
public struct J<T> public struct J<T>
{ {
private readonly T value; public T Value { get; }
public T Value
{
get { return value; }
}
[JsonConstructor] [JsonConstructor]
public J(T value) public J(T value)
{ {
this.value = value; Value = value;
} }
public static implicit operator T(J<T> value) public static implicit operator T(J<T> value)
{ {
return value.value; return value.Value;
} }
public static implicit operator J<T>(T d) public static implicit operator J<T>(T d)
@ -41,7 +37,7 @@ namespace Squidex.Infrastructure.Orleans
public override string ToString() public override string ToString()
{ {
return value?.ToString() ?? string.Empty; return Value?.ToString() ?? string.Empty;
} }
public static Task<J<T>> AsTask(T value) public static Task<J<T>> AsTask(T value)
@ -58,32 +54,38 @@ namespace Squidex.Infrastructure.Orleans
[SerializerMethod] [SerializerMethod]
public static void Serialize(object input, ISerializationContext context, Type expected) public static void Serialize(object input, ISerializationContext context, Type expected)
{ {
var stream = new MemoryStream(); using (Profile.Method(nameof(J)))
using (var writer = new JsonTextWriter(new StreamWriter(stream)))
{ {
J.Serializer.Serialize(writer, input); var stream = new MemoryStream();
writer.Flush(); using (var writer = new JsonTextWriter(new StreamWriter(stream)))
} {
J.Serializer.Serialize(writer, input);
var outBytes = stream.ToArray(); writer.Flush();
}
context.StreamWriter.Write(outBytes.Length); var outBytes = stream.ToArray();
context.StreamWriter.Write(outBytes);
context.StreamWriter.Write(outBytes.Length);
context.StreamWriter.Write(outBytes);
}
} }
[DeserializerMethod] [DeserializerMethod]
public static object Deserialize(Type expected, IDeserializationContext context) public static object Deserialize(Type expected, IDeserializationContext context)
{ {
var outLength = context.StreamReader.ReadInt(); using (Profile.Method(nameof(J)))
var outBytes = context.StreamReader.ReadBytes(outLength); {
var outLength = context.StreamReader.ReadInt();
var outBytes = context.StreamReader.ReadBytes(outLength);
var stream = new MemoryStream(outBytes); var stream = new MemoryStream(outBytes);
using (var reader = new JsonTextReader(new StreamReader(stream))) using (var reader = new JsonTextReader(new StreamReader(stream)))
{ {
return J.Serializer.Deserialize(reader, expected); return J.Serializer.Deserialize(reader, expected);
}
} }
} }
} }

11
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -8,13 +8,14 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.1" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0-rc2" /> <PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0-rc2" /> <PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.0.0-rc2" /> <PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NodaTime" Version="2.2.4" /> <PackageReference Include="NodaTime" Version="2.2.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0001" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0001" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />

2
src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs

@ -110,6 +110,8 @@ namespace Squidex.Infrastructure.UsageTracking
public async Task<long> GetMonthlyCallsAsync(string key, DateTime date) public async Task<long> GetMonthlyCallsAsync(string key, DateTime date)
{ {
Guard.NotNull(key, nameof(key));
ThrowIfDisposed(); ThrowIfDisposed();
var dateFrom = new DateTime(date.Year, date.Month, 1); var dateFrom = new DateTime(date.Year, date.Month, 1);

56
src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs

@ -0,0 +1,56 @@
// ==========================================================================
// 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;
namespace Squidex.Infrastructure.UsageTracking
{
public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker
{
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10);
private readonly IUsageTracker inner;
public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache)
: base(cache)
{
Guard.NotNull(inner, nameof(inner));
this.inner = inner;
}
public Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate)
{
return inner.QueryAsync(key, fromDate, toDate);
}
public Task TrackAsync(string key, double weight, double elapsedMs)
{
return inner.TrackAsync(key, weight, elapsedMs);
}
public async Task<long> GetMonthlyCallsAsync(string key, DateTime date)
{
Guard.NotNull(key, nameof(key));
var cacheKey = string.Concat(key, date);
if (Cache.TryGetValue<long>(cacheKey, out var result))
{
return result;
}
result = await inner.GetMonthlyCallsAsync(key, date);
Cache.Set(cacheKey, result, CacheTime);
return result;
}
}
}

2
src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -17,6 +17,8 @@ namespace Squidex.Shared.Identity
public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails"; public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails";
public static readonly string SquidexHidden = "urn:squidex:hidden";
public static readonly string Prefix = "urn:squidex:"; public static readonly string Prefix = "urn:squidex:";
} }
} }

3
src/Squidex.Shared/Squidex.Shared.csproj

@ -17,4 +17,7 @@
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
</Project> </Project>

5
src/Squidex.Shared/Users/IUserResolver.cs

@ -5,12 +5,15 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Squidex.Shared.Users namespace Squidex.Shared.Users
{ {
public interface IUserResolver public interface IUserResolver
{ {
Task<IUser> FindByIdAsync(string id); Task<IUser> FindByIdOrEmailAsync(string idOrEmail);
Task<List<IUser>> QueryByEmailAsync(string email);
} }
} }

13
src/Squidex.Domain.Users/UserExtensions.cs → src/Squidex.Shared/Users/UserExtensions.cs

@ -9,9 +9,8 @@ using System;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users namespace Squidex.Shared.Users
{ {
public static class UserExtensions public static class UserExtensions
{ {
@ -35,6 +34,11 @@ namespace Squidex.Domain.Users
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email)); user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email));
} }
public static void SetHidden(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexHidden, value.ToString());
}
public static void SetConsent(this IUser user) public static void SetConsent(this IUser user)
{ {
user.SetClaim(SquidexClaimTypes.SquidexConsent, "true"); user.SetClaim(SquidexClaimTypes.SquidexConsent, "true");
@ -45,6 +49,11 @@ namespace Squidex.Domain.Users
user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString()); user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString());
} }
public static bool IsHidden(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexHidden, "true");
}
public static bool HasConsent(this IUser user) public static bool HasConsent(this IUser user)
{ {
return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true"); return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true");

1
src/Squidex/Areas/Api/Controllers/ApiController.cs

@ -16,6 +16,7 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers namespace Squidex.Areas.Api.Controllers
{ {
[Area("Api")] [Area("Api")]
[ApiModelValidation(false)]
public abstract class ApiController : Controller public abstract class ApiController : Controller
{ {
protected ICommandBus CommandBus { get; } protected ICommandBus CommandBus { get; }

10
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -10,10 +10,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps namespace Squidex.Areas.Api.Controllers.Apps
@ -50,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetClients(string app) public IActionResult GetClients(string app)
{ {
var response = App.Clients.Select(x => SimpleMapper.Map(x.Value, new ClientDto { Id = x.Key })).ToList(); var response = App.Clients.Select(ClientDto.FromKvp).ToList();
Response.Headers["ETag"] = App.Version.ToString(); Response.Headers["ETag"] = App.Version.ToString();
@ -76,11 +74,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateAppClientDto request) public async Task<IActionResult> PostClient(string app, [FromBody] CreateAppClientDto request)
{ {
var command = SimpleMapper.Map(request, new AttachClient()); var command = request.ToCommand();
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
var response = SimpleMapper.Map(command, new ClientDto { Name = command.Id, Permission = AppClientPermission.Editor }); var response = ClientDto.FromCommand(command);
return CreatedAtAction(nameof(GetClients), new { app }, response); return CreatedAtAction(nameof(GetClients), new { app }, response);
} }
@ -104,7 +102,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request) public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request)
{ {
await CommandBus.PublishAsync(SimpleMapper.Map(request, new UpdateClient { Id = clientId })); await CommandBus.PublishAsync(request.ToCommand(clientId));
return NoContent(); return NoContent();
} }

17
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations; using NSwag.Annotations;
@ -13,7 +12,6 @@ using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps namespace Squidex.Areas.Api.Controllers.Apps
@ -50,9 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetContributors(string app) public IActionResult GetContributors(string app)
{ {
var contributors = App.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Permission = x.Value }).ToArray(); var response = ContributorsDto.FromApp(App, appPlansProvider);
var response = new ContributorsDto { Contributors = contributors, MaxContributors = appPlansProvider.GetPlanForApp(App).MaxContributors };
Response.Headers["ETag"] = App.Version.ToString(); Response.Headers["ETag"] = App.Version.ToString();
@ -65,19 +61,24 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="request">Contributor object that needs to be added to the app.</param> /// <param name="request">Contributor object that needs to be added to the app.</param>
/// <returns> /// <returns>
/// 204 => User assigned to app. /// 200 => User assigned to app.
/// 400 => User is already assigned to the app or not found. /// 400 => User is already assigned to the app or not found.
/// 404 => App not found. /// 404 => App not found.
/// </returns> /// </returns>
[HttpPost] [HttpPost]
[Route("apps/{app}/contributors/")] [Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorAssignedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)] [ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignAppContributorDto request) public async Task<IActionResult> PostContributor(string app, [FromBody] AssignAppContributorDto request)
{ {
await CommandBus.PublishAsync(SimpleMapper.Map(request, new AssignContributor())); var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command);
return NoContent(); var result = context.Result<EntityCreatedResult<string>>();
var response = ContributorAssignedDto.FromId(result.IdOrValue);
return Ok(response);
} }
/// <summary> /// <summary>

21
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -6,17 +6,13 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps namespace Squidex.Areas.Api.Controllers.Apps
@ -50,14 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetLanguages(string app) public IActionResult GetLanguages(string app)
{ {
var response = App.LanguagesConfig.OfType<LanguageConfig>().Select(x => var response = AppLanguageDto.FromApp(App);
SimpleMapper.Map(x.Language,
new AppLanguageDto
{
IsMaster = x == App.LanguagesConfig.Master,
IsOptional = x.IsOptional,
Fallback = x.LanguageFallbacks.ToList()
})).OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code).ToList();
Response.Headers["ETag"] = App.Version.ToString(); Response.Headers["ETag"] = App.Version.ToString();
@ -82,9 +71,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddAppLanguageDto request) public async Task<IActionResult> PostLanguage(string app, [FromBody] AddAppLanguageDto request)
{ {
await CommandBus.PublishAsync(SimpleMapper.Map(request, new AddLanguage())); var command = request.ToCommand();
var response = SimpleMapper.Map(request.Language, new AppLanguageDto { Fallback = new List<Language>() }); await CommandBus.PublishAsync(command);
var response = AppLanguageDto.FromCommand(command);
return CreatedAtAction(nameof(GetLanguages), new { app }, response); return CreatedAtAction(nameof(GetLanguages), new { app }, response);
} }
@ -106,7 +97,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateAppLanguageDto request) public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateAppLanguageDto request)
{ {
await CommandBus.PublishAsync(SimpleMapper.Map(request, new UpdateLanguage { Language = language })); await CommandBus.PublishAsync(request.ToCommand(ParseLanguage(language)));
return NoContent(); return NoContent();
} }

15
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -13,7 +13,6 @@ using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps namespace Squidex.Areas.Api.Controllers.Apps
@ -50,9 +49,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetPatterns(string app) public IActionResult GetPatterns(string app)
{ {
var response = var response = App.Patterns.Select(AppPatternDto.FromKvp).OrderBy(x => x.Name).ToList();
App.Patterns.Select(x => SimpleMapper.Map(x.Value, new AppPatternDto { PatternId = x.Key }))
.OrderBy(x => x.Name).ToList(); Response.Headers["ETag"] = App.Version.ToString();
return Ok(response); return Ok(response);
} }
@ -73,11 +72,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request) public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request)
{ {
var command = SimpleMapper.Map(request, new AddPattern()); var command = request.ToAddCommand();
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
var response = SimpleMapper.Map(request, new AppPatternDto { PatternId = command.PatternId }); var response = AppPatternDto.FromCommand(command);
return CreatedAtAction(nameof(GetPatterns), new { app }, response); return CreatedAtAction(nameof(GetPatterns), new { app }, response);
} }
@ -99,9 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request) public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request)
{ {
var command = SimpleMapper.Map(request, new UpdatePattern { PatternId = id }); await CommandBus.PublishAsync(request.ToUpdateCommand(id));
await CommandBus.PublishAsync(command);
return NoContent(); return NoContent();
} }

27
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -11,12 +11,10 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Pipeline; using Squidex.Pipeline;
@ -60,19 +58,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
{ {
var subject = HttpContext.User.OpenIdSubject(); var subject = HttpContext.User.OpenIdSubject();
var apps = await appProvider.GetUserApps(subject); var entities = await appProvider.GetUserApps(subject);
var response = apps.Select(a => var response = entities.Select(a => AppDto.FromApp(a, subject, appPlansProvider)).ToList();
{
var dto = SimpleMapper.Map(a, new AppDto());
dto.Permission = a.Contributors[subject];
dto.PlanName = appPlansProvider.GetPlanForApp(a)?.Name;
dto.PlanUpgrade = appPlansProvider.GetPlanUpgradeForApp(a)?.Name;
return dto;
}).ToList();
return Ok(response); return Ok(response);
} }
@ -98,17 +86,10 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostApp([FromBody] CreateAppDto request) public async Task<IActionResult> PostApp([FromBody] CreateAppDto request)
{ {
var command = SimpleMapper.Map(request, new CreateApp()); var context = await CommandBus.PublishAsync(request.ToCommand());
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>(); var result = context.Result<EntityCreatedResult<Guid>>();
var response = new AppCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; var response = AppCreatedDto.FromResult(result, appPlansProvider);
response.Permission = AppContributorPermission.Owner;
response.PlanName = appPlansProvider.GetPlan(null)?.Name;
response.PlanUpgrade = appPlansProvider.GetPlanUpgrade(null)?.Name;
return CreatedAtAction(nameof(GetApps), response); return CreatedAtAction(nameof(GetApps), response);
} }

7
src/Squidex/Areas/Api/Controllers/Apps/Models/AddAppLanguageDto.cs

@ -6,7 +6,9 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -17,5 +19,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
[Required] [Required]
public Language Language { get; set; } public Language Language { get; set; }
public AddLanguage ToCommand()
{
return SimpleMapper.Map(this, new AddLanguage());
}
} }
} }

25
src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs

@ -5,10 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -20,17 +23,17 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
public string Id { get; set; } public string Id { get; set; }
/// <summary>
/// The new version of the entity.
/// </summary>
public long Version { get; set; }
/// <summary> /// <summary>
/// The permission level of the user. /// The permission level of the user.
/// </summary> /// </summary>
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public AppContributorPermission Permission { get; set; } public AppContributorPermission Permission { get; set; }
/// <summary>
/// The new version of the entity.
/// </summary>
public long Version { get; set; }
/// <summary> /// <summary>
/// Gets the current plan name. /// Gets the current plan name.
/// </summary> /// </summary>
@ -40,5 +43,17 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Gets the next plan name. /// Gets the next plan name.
/// </summary> /// </summary>
public string PlanUpgrade { get; set; } public string PlanUpgrade { get; set; }
public static AppCreatedDto FromResult(EntityCreatedResult<Guid> result, IAppPlansProvider apps)
{
var response = new AppCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version };
response.Permission = AppContributorPermission.Owner;
response.PlanName = apps.GetPlan(null)?.Name;
response.PlanUpgrade = apps.GetPlanUpgrade(null)?.Name;
return response;
}
} }
} }

15
src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -11,6 +11,9 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -58,5 +61,17 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Gets the next plan name. /// Gets the next plan name.
/// </summary> /// </summary>
public string PlanUpgrade { get; set; } public string PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string subject, IAppPlansProvider plans)
{
var response = SimpleMapper.Map(app, new AppDto());
response.Permission = app.Contributors[subject];
response.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;
return response;
}
} }
} }

26
src/Squidex/Areas/Api/Controllers/Apps/Models/AppLanguageDto.cs

@ -7,7 +7,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -40,5 +45,26 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Indicates if the language is optional. /// Indicates if the language is optional.
/// </summary> /// </summary>
public bool IsOptional { get; set; } public bool IsOptional { get; set; }
public static AppLanguageDto FromCommand(AddLanguage command)
{
return SimpleMapper.Map(command.Language, new AppLanguageDto { Fallback = new List<Language>() });
}
public static AppLanguageDto[] FromApp(IAppEntity app)
{
return app.LanguagesConfig.OfType<LanguageConfig>().Select(x => FromLanguage(x, app)).OrderByDescending(x => x.IsMaster).ThenBy(x => x.Iso2Code).ToArray();
}
private static AppLanguageDto FromLanguage(LanguageConfig x, IAppEntity app)
{
return SimpleMapper.Map(x.Language,
new AppLanguageDto
{
IsMaster = x == app.LanguagesConfig.Master,
IsOptional = x.IsOptional,
Fallback = x.LanguageFallbacks.ToList()
});
}
} }
} }

14
src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs

@ -6,7 +6,11 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -33,5 +37,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The regex message. /// The regex message.
/// </summary> /// </summary>
public string Message { get; set; } public string Message { get; set; }
public static AppPatternDto FromKvp(KeyValuePair<Guid, AppPattern> kvp)
{
return SimpleMapper.Map(kvp.Value, new AppPatternDto { PatternId = kvp.Key });
}
public static AppPatternDto FromCommand(AddPattern command)
{
return SimpleMapper.Map(command, new AppPatternDto());
}
} }
} }

9
src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs

@ -9,13 +9,15 @@ using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
public sealed class AssignAppContributorDto public sealed class AssignAppContributorDto
{ {
/// <summary> /// <summary>
/// The id of the user to add to the app. /// The id or email of the user to add to the app.
/// </summary> /// </summary>
[Required] [Required]
public string ContributorId { get; set; } public string ContributorId { get; set; }
@ -25,5 +27,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public AppContributorPermission Permission { get; set; } public AppContributorPermission Permission { get; set; }
public AssignContributor ToCommand()
{
return SimpleMapper.Map(this, new AssignContributor());
}
} }
} }

13
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -5,10 +5,13 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -38,5 +41,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public AppClientPermission Permission { get; set; } public AppClientPermission Permission { get; set; }
public static ClientDto FromKvp(KeyValuePair<string, AppClient> kvp)
{
return SimpleMapper.Map(kvp.Value, new ClientDto { Id = kvp.Key });
}
public static ClientDto FromCommand(AttachClient command)
{
return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Permission = AppClientPermission.Editor });
}
} }
} }

25
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorAssignedDto.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class ContributorAssignedDto
{
/// <summary>
/// The id of the user that has been assigned as contributor.
/// </summary>
[Required]
public string ContributorId { get; set; }
public static ContributorAssignedDto FromId(string id)
{
return new ContributorAssignedDto { ContributorId = id };
}
}
}

12
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -6,6 +6,9 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; 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.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -21,5 +24,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The maximum number of allowed contributors. /// The maximum number of allowed contributors.
/// </summary> /// </summary>
public int MaxContributors { get; set; } public int MaxContributors { get; set; }
public static ContributorsDto FromApp(IAppEntity app, IAppPlansProvider plans)
{
var plan = plans.GetPlanForApp(app);
var contributors = app.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Permission = x.Value }).ToArray();
return new ContributorsDto { Contributors = contributors, MaxContributors = plan.MaxContributors };
}
} }
} }

7
src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppClientDto.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -17,5 +19,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required] [Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Id { get; set; } public string Id { get; set; }
public AttachClient ToCommand()
{
return SimpleMapper.Map(this, new AttachClient());
}
} }
} }

7
src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppDto.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -22,5 +24,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Initialize the app with the inbuilt template. /// Initialize the app with the inbuilt template.
/// </summary> /// </summary>
public string Template { get; set; } public string Template { get; set; }
public CreateApp ToCommand()
{
return SimpleMapper.Map(this, new CreateApp());
}
} }
} }

7
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs

@ -9,6 +9,8 @@ using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -25,5 +27,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
public AppClientPermission? Permission { get; set; } public AppClientPermission? Permission { get; set; }
public UpdateClient ToCommand(string clientId)
{
return SimpleMapper.Map(this, new UpdateClient { Id = clientId });
}
} }
} }

7
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppLanguageDto.cs

@ -6,7 +6,9 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -26,5 +28,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Optional fallback languages. /// Optional fallback languages.
/// </summary> /// </summary>
public List<Language> Fallback { get; set; } public List<Language> Fallback { get; set; }
public UpdateLanguage ToCommand(Language language)
{
return SimpleMapper.Map(this, new UpdateLanguage { Language = language });
}
} }
} }

13
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs

@ -5,7 +5,10 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -27,5 +30,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The regex message. /// The regex message.
/// </summary> /// </summary>
public string Message { get; set; } public string Message { get; set; }
public AddPattern ToAddCommand()
{
return SimpleMapper.Map(this, new AddPattern());
}
public UpdatePattern ToUpdateCommand(Guid id)
{
return SimpleMapper.Map(this, new UpdatePattern { PatternId = id });
}
} }
} }

15
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -21,7 +21,6 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Assets namespace Squidex.Areas.Api.Controllers.Assets
@ -93,11 +92,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
await assetRepository.QueryAsync(App.Id, idsList) : await assetRepository.QueryAsync(App.Id, idsList) :
await assetRepository.QueryAsync(App.Id, Request.QueryString.ToString()); await assetRepository.QueryAsync(App.Id, Request.QueryString.ToString());
var response = new AssetsDto var response = AssetsDto.FromAssets(assets);
{
Total = assets.Total,
Items = assets.Select(x => SimpleMapper.Map(x, new AssetDto { FileType = x.FileName.FileType() })).ToArray()
};
Response.Headers["Surrogate-Key"] = string.Join(" ", response.Items.Select(x => x.Id)); Response.Headers["Surrogate-Key"] = string.Join(" ", response.Items.Select(x => x.Id));
@ -127,7 +122,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NotFound(); return NotFound();
} }
var response = SimpleMapper.Map(entity, new AssetDto { FileType = entity.FileName.FileType() }); var response = AssetDto.FromAsset(entity);
Response.Headers["ETag"] = entity.Version.ToString(); Response.Headers["ETag"] = entity.Version.ToString();
Response.Headers["Surrogate-Key"] = entity.Id.ToString(); Response.Headers["Surrogate-Key"] = entity.Id.ToString();
@ -161,7 +156,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>(); var result = context.Result<EntityCreatedResult<Guid>>();
var response = AssetCreatedDto.Create(command, result); var response = AssetCreatedDto.FromCommand(command, result);
return StatusCode(201, response); return StatusCode(201, response);
} }
@ -217,9 +212,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request) public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request)
{ {
var command = SimpleMapper.Map(request, new RenameAsset { AssetId = id }); await CommandBus.PublishAsync(request.ToCommand(id));
await CommandBus.PublishAsync(command);
return NoContent(); return NoContent();
} }

4
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs

@ -8,6 +8,7 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -67,13 +68,14 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static AssetCreatedDto Create(CreateAsset command, EntityCreatedResult<Guid> result) public static AssetCreatedDto FromCommand(CreateAsset command, EntityCreatedResult<Guid> result)
{ {
var response = new AssetCreatedDto var response = new AssetCreatedDto
{ {
Id = command.AssetId, Id = command.AssetId,
FileName = command.File.FileName, FileName = command.File.FileName,
FileSize = command.File.FileSize, FileSize = command.File.FileSize,
FileType = command.File.FileName.FileType(),
FileVersion = result.Version, FileVersion = result.Version,
MimeType = command.File.MimeType, MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null, IsImage = command.ImageInfo != null,

7
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -8,7 +8,9 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
{ {
@ -88,5 +90,10 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// The version of the asset. /// The version of the asset.
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static AssetDto FromAsset(IAssetEntity asset)
{
return SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
}
} }
} }

8
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs

@ -5,7 +5,10 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
{ {
@ -16,5 +19,10 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary> /// </summary>
[Required] [Required]
public string FileName { get; set; } public string FileName { get; set; }
public RenameAsset ToCommand(Guid id)
{
return SimpleMapper.Map(this, new RenameAsset { AssetId = id });
}
} }
} }

19
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs

@ -5,18 +5,29 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Controllers.Assets.Models namespace Squidex.Areas.Api.Controllers.Assets.Models
{ {
public sealed class AssetsDto public sealed class AssetsDto
{ {
/// <summary> /// <summary>
/// The total number of assets. /// The assets.
/// </summary> /// </summary>
public long Total { get; set; } [Required]
public AssetDto[] Items { get; set; }
/// <summary> /// <summary>
/// The assets. /// The total number of assets.
/// </summary> /// </summary>
public AssetDto[] Items { get; set; } public long Total { get; set; }
public static AssetsDto FromAssets(IResultList<IAssetEntity> assets)
{
return new AssetsDto { Total = assets.Total, Items = assets.Select(AssetDto.FromAsset).ToArray() };
}
} }
} }

5
src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -15,7 +15,6 @@ using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models; using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline; using Squidex.Pipeline;
@ -57,7 +56,9 @@ namespace Squidex.Areas.Api.Controllers.Backups
var jobs = await backupGrain.GetStateAsync(); var jobs = await backupGrain.GetStateAsync();
return Ok(jobs.Value.Select(x => SimpleMapper.Map(x, new BackupJobDto())).ToList()); var response = jobs.Value.Select(BackupJobDto.FromBackup).ToList();
return Ok(response);
} }
/// <summary> /// <summary>

7
src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs

@ -7,6 +7,8 @@
using System; using System;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Backups.Models namespace Squidex.Areas.Api.Controllers.Backups.Models
{ {
@ -41,5 +43,10 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
/// Indicates if the job has failed. /// Indicates if the job has failed.
/// </summary> /// </summary>
public bool IsFailed { get; set; } public bool IsFailed { get; set; }
public static BackupJobDto FromBackup(IBackupJob backup)
{
return SimpleMapper.Map(backup, new BackupJobDto());
}
} }
} }

79
src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs

@ -15,7 +15,6 @@ using NodaTime.Text;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.Contents.GraphQL;
@ -119,23 +118,8 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = new ContentsDto var response = new ContentsDto
{ {
Total = result.Contents.Total, Total = result.Total,
Items = result.Contents.Take(200).Select(item => Items = result.Take(200).Select(item => SimpleMapper.Map(item, new ContentDto { Data = item.Data })).ToArray()
{
var itemModel = SimpleMapper.Map(item, new ContentDto());
if (item.Data != null)
{
itemModel.Data = item.Data.ToApiModel(result.Schema.SchemaDef, App.LanguagesConfig, !isFrontendClient);
}
if (item.PendingData != null && isFrontendClient)
{
itemModel.PendingData = item.PendingData.ToApiModel(result.Schema.SchemaDef, App.LanguagesConfig, !isFrontendClient);
}
return itemModel;
}).ToArray()
}; };
Response.Headers["Surrogate-Key"] = string.Join(" ", response.Items.Select(x => x.Id)); Response.Headers["Surrogate-Key"] = string.Join(" ", response.Items.Select(x => x.Id));
@ -162,24 +146,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, Guid id) public async Task<IActionResult> GetContent(string app, string name, Guid id)
{ {
var (schema, entity) = await contentQuery.FindContentAsync(App, name, User, id); var content = await contentQuery.FindContentAsync(App, name, User, id);
var response = SimpleMapper.Map(entity, new ContentDto());
if (entity.Data != null) var response = SimpleMapper.Map(content, new ContentDto { Data = content.Data });
{
var isFrontendClient = User.IsFrontendClient();
if (entity.Data != null)
{
response.Data = entity.Data.ToApiModel(schema.SchemaDef, App.LanguagesConfig, !isFrontendClient);
}
if (entity.PendingData != null && isFrontendClient)
{
response.PendingData = entity.PendingData.ToApiModel(schema.SchemaDef, App.LanguagesConfig, !isFrontendClient);
}
}
Response.Headers["ETag"] = entity.Version.ToString(); Response.Headers["ETag"] = entity.Version.ToString();
Response.Headers["Surrogate-Key"] = entity.Id.ToString(); Response.Headers["Surrogate-Key"] = entity.Id.ToString();
@ -210,24 +179,10 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
var (schema, entity) = await contentQuery.FindContentAsync(App, name, User, id, version); var (schema, entity) = await contentQuery.FindContentAsync(App, name, User, id, version);
var response = SimpleMapper.Map(entity, new ContentDto()); var response = SimpleMapper.Map(content, new ContentDto { Data = content.Data });
if (entity.Data != null)
{
var isFrontendClient = User.IsFrontendClient();
if (entity.Data != null) Response.Headers["ETag"] = content.Version.ToString();
{ Response.Headers["Surrogate-Key"] = content.Id.ToString();
response.Data = entity.Data.ToApiModel(schema.SchemaDef, App.LanguagesConfig, !isFrontendClient);
}
if (entity.PendingData != null && isFrontendClient)
{
response.PendingData = entity.PendingData.ToApiModel(schema.SchemaDef, App.LanguagesConfig, !isFrontendClient);
}
}
Response.Headers["ETag"] = version.ToString();
return Ok(response.Data); return Ok(response.Data);
} }
@ -253,14 +208,14 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<NamedContentData>>(); var result = context.Result<EntityCreatedResult<NamedContentData>>();
var response = ContentDto.Create(command, result); var response = ContentDto.FromCommand(command, result);
return CreatedAtAction(nameof(GetContent), new { id = command.ContentId }, response); return CreatedAtAction(nameof(GetContent), new { id = command.ContentId }, response);
} }
@ -287,10 +242,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asProposal = false) public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asProposal = false)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsProposal = asProposal }; var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsProposal = asProposal };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>(); var result = context.Result<ContentDataChangedResult>();
@ -321,10 +275,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asProposal = false) public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asProposal = false)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsProposal = asProposal }; var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsProposal = asProposal };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>(); var result = context.Result<ContentDataChangedResult>();
@ -354,7 +307,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PublishContent(string app, string name, Guid id, string dueTime = null) public async Task<IActionResult> PublishContent(string app, string name, Guid id, string dueTime = null)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = CreateCommand(id, Status.Published, dueTime); var command = CreateCommand(id, Status.Published, dueTime);
@ -384,7 +337,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> UnpublishContent(string app, string name, Guid id, string dueTime = null) public async Task<IActionResult> UnpublishContent(string app, string name, Guid id, string dueTime = null)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = CreateCommand(id, Status.Draft, dueTime); var command = CreateCommand(id, Status.Draft, dueTime);
@ -414,7 +367,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> ArchiveContent(string app, string name, Guid id, string dueTime = null) public async Task<IActionResult> ArchiveContent(string app, string name, Guid id, string dueTime = null)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = CreateCommand(id, Status.Archived, dueTime); var command = CreateCommand(id, Status.Archived, dueTime);
@ -444,7 +397,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> RestoreContent(string app, string name, Guid id, string dueTime = null) public async Task<IActionResult> RestoreContent(string app, string name, Guid id, string dueTime = null)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = CreateCommand(id, Status.Draft, dueTime); var command = CreateCommand(id, Status.Draft, dueTime);
@ -501,7 +454,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, Guid id) public async Task<IActionResult> DeleteContent(string app, string name, Guid id)
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.ThrowIfSchemaNotExistsAsync(App, name);
var command = new DeleteContent { ContentId = id }; var command = new DeleteContent { ContentId = id };

4
src/Squidex/Areas/Api/Controllers/Content/Models/ContentDto.cs

@ -71,7 +71,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
public Instant LastModified { get; set; } public Instant LastModified { get; set; }
/// <summary> /// <summary>
/// Gets the status of the content. /// The the status of the content.
/// </summary> /// </summary>
public Status Status { get; set; } public Status Status { get; set; }
@ -80,7 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static ContentDto Create(CreateContent command, EntityCreatedResult<NamedContentData> result) public static ContentDto FromCommand(CreateContent command, EntityCreatedResult<NamedContentData> result)
{ {
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();

5
src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs

@ -13,7 +13,6 @@ using Orleans;
using Squidex.Areas.Api.Controllers.EventConsumers.Models; using Squidex.Areas.Api.Controllers.EventConsumers.Models;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.EventConsumers namespace Squidex.Areas.Api.Controllers.EventConsumers
@ -39,9 +38,9 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{ {
var entities = await eventConsumerManagerGrain.GetConsumersAsync(); var entities = await eventConsumerManagerGrain.GetConsumersAsync();
var models = entities.Value.Select(x => SimpleMapper.Map(x, new EventConsumerDto())).ToList(); var response = entities.Value.Select(EventConsumerDto.FromEventConsumerInfo).ToList();
return Ok(models); return Ok(response);
} }
[HttpPut] [HttpPut]

8
src/Squidex/Areas/Api/Controllers/EventConsumers/Models/EventConsumerDto.cs

@ -5,6 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.EventConsumers.Models namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
{ {
public sealed class EventConsumerDto public sealed class EventConsumerDto
@ -18,5 +21,10 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
public string Error { get; set; } public string Error { get; set; }
public string Position { get; set; } public string Position { get; set; }
public static EventConsumerDto FromEventConsumerInfo(EventConsumerInfo eventConsumerInfo)
{
return SimpleMapper.Map(eventConsumerInfo, new EventConsumerDto());
}
} }
} }

3
src/Squidex/Areas/Api/Controllers/History/HistoryController.cs

@ -12,7 +12,6 @@ using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.History.Models; using Squidex.Areas.Api.Controllers.History.Models;
using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Domain.Apps.Entities.History.Repositories;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.History namespace Squidex.Areas.Api.Controllers.History
@ -52,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.History
{ {
var entities = await historyEventRepository.QueryByChannelAsync(App.Id, channel, 100); var entities = await historyEventRepository.QueryByChannelAsync(App.Id, channel, 100);
var response = entities.Select(x => SimpleMapper.Map(x, new HistoryEventDto())).ToList(); var response = entities.Select(HistoryEventDto.FromHistoryEvent).ToList();
return Ok(response); return Ok(response);
} }

7
src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs

@ -8,6 +8,8 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.History.Models namespace Squidex.Areas.Api.Controllers.History.Models
{ {
@ -45,5 +47,10 @@ namespace Squidex.Areas.Api.Controllers.History.Models
/// The version identifier. /// The version identifier.
/// </summary> /// </summary>
public long Version { get; set; } public long Version { get; set; }
public static HistoryEventDto FromHistoryEvent(IHistoryEventEntity x)
{
return SimpleMapper.Map(x, new HistoryEventDto());
}
} }
} }

7
src/Squidex/Areas/Api/Controllers/LanguageDto.cs

@ -6,6 +6,8 @@
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers namespace Squidex.Areas.Api.Controllers
{ {
@ -22,5 +24,10 @@ namespace Squidex.Areas.Api.Controllers
/// </summary> /// </summary>
[Required] [Required]
public string EnglishName { get; set; } public string EnglishName { get; set; }
public static LanguageDto FromLanguage(Language language)
{
return SimpleMapper.Map(language, new LanguageDto());
}
} }
} }

3
src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs

@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Languages namespace Squidex.Areas.Api.Controllers.Languages
@ -43,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Languages
[ApiCosts(0)] [ApiCosts(0)]
public IActionResult GetLanguages() public IActionResult GetLanguages()
{ {
var response = Language.AllLanguages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList(); var response = Language.AllLanguages.Select(LanguageDto.FromLanguage).ToList();
return Ok(response); return Ok(response);
} }

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

Loading…
Cancel
Save