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

5
.drone.yml

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

2
Dockerfile

@ -1,7 +1,7 @@
#
# 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

27
Dockerfile.build

@ -1,30 +1,7 @@
FROM microsoft/aspnetcore-build:2.0.0
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
&& mkdir /srv/var \
&& mkdir /tmp/phantomjs \
# Download Phantom JS
&& curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
# Copy binaries only
&& mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
# Create symbol link
# Clean up
&& apt-get autoremove -y \
&& apt-get clean all \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN phantomjs --version
FROM squidex/aspnetcore-build-phantomjs-chromium:2.0.3-jessie-fix1 as builder
COPY src/Squidex/package.json /tmp/package.json
RUN cd /tmp \
&& npm install \
&& npm rebuild node-sass

2
LICENSE.txt

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

1
Squidex.ruleset

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

28
libs/Dockerfile

@ -6,7 +6,7 @@ RUN apt-get update \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install official PhantomJS release
# Install official PhantomJS release
RUN set -x \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
@ -22,4 +22,28 @@ RUN set -x \
&& apt-get clean all \
&& 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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fody" Version="2.4.6" />
<PackageReference Include="Freezable.Fody" Version="1.8.0" />
<PackageReference Include="Fody" Version="3.0.3" />
<PackageReference Include="Freezable.Fody" Version="1.9.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</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.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
@ -106,7 +107,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
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(languagesConfig, nameof(languagesConfig));
@ -123,6 +124,32 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
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 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.
// =========================================-=================================
using System;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
@ -15,6 +17,7 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Shared.Users;
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 TimestampDateTimePlaceholder = "$TIMESTAMP_DATETIME";
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 TimeSpan UserCacheDuration = TimeSpan.FromMinutes(10);
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(urlGenerator, nameof(urlGenerator));
Guard.NotNull(userResolver, nameof(userResolver));
this.memoryCache = memoryCache;
this.serializer = serializer;
this.userResolver = userResolver;
this.urlGenerator = urlGenerator;
}
public virtual JToken ToRouteData(object value)
@ -75,6 +91,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
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);
var result = sb.ToString();
@ -92,6 +114,39 @@ namespace Squidex.Domain.Apps.Core.HandleRules
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)
{
switch (@event.Payload)
@ -166,5 +221,24 @@ namespace Squidex.Domain.Apps.Core.HandleRules
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.Events\Squidex.Domain.Apps.Events.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="4.2.2" />
<PackageReference Include="Elasticsearch.Net" Version="6.0.2" />
<PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.4.4" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<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="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.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>
<PropertyGroup>
<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)
{
this.Value = value;
Value = value;
}
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
{
private readonly IAppProvider appProvider;
private readonly IMongoCollection<MongoContentEntity> archiveCollection;
protected IMongoCollection<MongoContentEntity> ArchiveCollection
{
get { return archiveCollection; }
}
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider)
: base(database)
@ -39,8 +33,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
archiveCollection = database.GetCollection<MongoContentEntity>("States_Contents_Archive");
}
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 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(
Index
.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();
}
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)
{
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");
await base.ClearAsync();
return Database.DropCollectionAsync("States_Contents_Archive");
}
}
}

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}";
await ArchiveCollection.ReplaceOneAsync(x => x.DocumentId == document.DocumentId, document, Upsert);
}
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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.4.1" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.4.4" />
<PackageReference Include="MongoDB.Driver" Version="2.5.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<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.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Entities
@ -26,128 +28,180 @@ namespace Squidex.Domain.Apps.Entities
private readonly IGrainFactory grainFactory;
private readonly IAppRepository appRepository;
private readonly IRuleRepository ruleRepository;
private readonly IRequestCache requestCache;
private readonly ISchemaRepository schemaRepository;
public AppProvider(
IGrainFactory grainFactory,
IAppRepository appRepository,
ISchemaRepository schemaRepository,
IRuleRepository ruleRepository)
IRuleRepository ruleRepository,
IRequestCache requestCache)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(appRepository, nameof(appRepository));
Guard.NotNull(schemaRepository, nameof(schemaRepository));
Guard.NotNull(requestCache, nameof(requestCache));
Guard.NotNull(ruleRepository, nameof(ruleRepository));
this.grainFactory = grainFactory;
this.appRepository = appRepository;
this.schemaRepository = schemaRepository;
this.requestCache = requestCache;
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();
if (!IsExisting(app))
return requestCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () =>
{
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))
{
return (null, null);
}
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
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);
if (appId == Guid.Empty)
return requestCache.GetOrCreateAsync($"GetAppAsync({appName})", async () =>
{
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))
{
return null;
}
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
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);
if (schemaId == Guid.Empty)
return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () =>
{
return null;
}
return await GetSchemaAsync(appId, schemaId, false);
using (Profile.Method<AppProvider>())
{
var schemaId = await GetSchemaIdAsync(appId, name);
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();
if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId)
return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () =>
{
return null;
}
return schema.Value;
using (Profile.Method<AppProvider>())
{
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
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 =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync()));
var schemas =
await Task.WhenAll(
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 =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync()));
var rules =
await Task.WhenAll(
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 =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
var apps =
await Task.WhenAll(
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)
{
return await schemaRepository.FindSchemaIdAsync(appId, name);
using (Profile.Method<AppProvider>())
{
return await schemaRepository.FindSchemaIdAsync(appId, name);
}
}
private static bool IsFound(IEntityWithVersion entity)
@ -155,14 +209,14 @@ namespace Squidex.Domain.Apps.Entities
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.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
@ -36,11 +37,12 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppGrain(
InitialPatterns initialPatterns,
IStore<Guid> store,
ISemanticLog log,
IAppProvider appProvider,
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver)
: base(store)
: base(store, log)
{
Guard.NotNull(initialPatterns, nameof(initialPatterns));
Guard.NotNull(appProvider, nameof(appProvider));
@ -70,11 +72,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
});
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));
AssignContributor(c);
return EntityCreatedResult.Create(c.ContributorId, NewVersion);
});
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.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Apps;
@ -34,20 +35,31 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
}
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)));
}
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 YearlyCosts { get; }
string YearlyId { get; }
long MaxApiCalls { 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 YearlyCosts { get; set; }
public string YearlyId { get; set; }
public long MaxApiCalls { 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
};
private readonly Dictionary<string, ConfigAppLimitsPlan> plansById;
private readonly List<ConfigAppLimitsPlan> plansList;
private readonly Dictionary<string, ConfigAppLimitsPlan> plansById = new Dictionary<string, ConfigAppLimitsPlan>(StringComparer.OrdinalIgnoreCase);
private readonly List<ConfigAppLimitsPlan> plansList = new List<ConfigAppLimitsPlan>();
public ConfigAppPlansProvider(IEnumerable<ConfigAppLimitsPlan> config)
{
Guard.NotNull(config, nameof(config));
plansList = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToList();
plansById = plansList.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase);
foreach (var plan in config.OrderBy(x => x.MaxApiCalls).Select(x => x.Clone()))
{
plansList.Add(plan);
plansById[plan.Id] = plan;
if (!string.IsNullOrWhiteSpace(plan.YearlyId) && !string.IsNullOrWhiteSpace(plan.YearlyCosts))
{
plansById[plan.YearlyId] = plan;
}
}
}
public IEnumerable<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.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
@ -23,8 +24,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetGrain : SquidexDomainObjectGrain<AssetState>, IAssetGrain
{
public AssetGrain(IStore<Guid> store)
: base(store)
public AssetGrain(IStore<Guid> store, ISemanticLog log)
: 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.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
@ -33,11 +34,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
public ContentGrain(
IStore<Guid> store,
ISemanticLog log,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentRepository contentRepository)
: base(store)
: base(store, log)
{
Guard.NotNull(appProvider, nameof(appProvider));
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.UriParser;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Edm;
@ -27,113 +28,130 @@ namespace Squidex.Domain.Apps.Entities.Contents
public sealed class ContentQueryService : IContentQueryService
{
private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IAppProvider appProvider;
private readonly IScriptEngine scriptEngine;
private readonly EdmModelBuilder modelBuilder;
public ContentQueryService(
IContentRepository contentRepository,
IContentVersionLoader contentVersionLoader,
IAppProvider appProvider,
IScriptEngine scriptEngine,
EdmModelBuilder modelBuilder)
{
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(modelBuilder, nameof(modelBuilder));
Guard.NotNull(appProvider, nameof(appProvider));
this.contentRepository = contentRepository;
this.contentVersionLoader = contentVersionLoader;
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
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(user, nameof(user));
Guard.NotEmpty(id, nameof(id));
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 =
version > EtagVersion.Empty ?
await contentRepository.FindContentAsync(app, schema, id, version) :
await contentRepository.FindContentAsync(app, schema, id);
isVersioned ?
await FindContentByVersionAsync(id, version) :
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));
}
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(user, nameof(user));
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 parsedStatus = ParseStatus(user, archived);
var parsedStatus = ParseStatus(isFrontendClient, archived);
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(app, nameof(app));
Guard.NotNull(user, nameof(user));
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);
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;
if (!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;
var isScripting = !string.IsNullOrWhiteSpace(scriptText);
yield return contentResult;
}
}
else
foreach (var content in contents)
{
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));
@ -175,11 +193,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
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>();
if (user.IsInClient("squidex-frontend"))
if (isFrontendClient)
{
if (archived)
{
@ -198,5 +216,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
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 GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
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 StatusType = new EnumerationGraphType<Status>();
public static readonly IGraphType NonNullInt = new NonNullGraphType(new IntGraphType());
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 NonNullStatusType = new NonNullGraphType(new EnumerationGraphType<Status>());
public static readonly IGraphType ListOfNonNullGuid = new ListGraphType(new NonNullGraphType(new GuidGraphType()));
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."
});
AddField(new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullStatusType,
Resolver = Resolve(x => x.Status),
Description = $"The the status of the {schemaName} content."
});
AddField(new FieldType
{
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.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
{
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)
{
content = (await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id)).Content;
content = await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id);
if (content != null)
{
@ -96,12 +96,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
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;
}
return result.Contents;
return result;
}
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);
foreach (var content in result.Contents)
foreach (var content in result)
{
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, long version);
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; }
[JsonProperty]
public long Version { get; set; }
public long Version { get; set; } = EtagVersion.Empty;
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.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
@ -25,8 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Rules
{
private readonly IAppProvider appProvider;
public RuleGrain(IStore<Guid> store, IAppProvider appProvider)
: base(store)
public RuleGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider)
: base(store, log)
{
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.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
@ -29,8 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
private readonly IAppProvider appProvider;
private readonly FieldRegistry registry;
public SchemaGrain(IStore<Guid> store, IAppProvider appProvider, FieldRegistry registry)
: base(store)
public SchemaGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, FieldRegistry registry)
: base(store, log)
{
Guard.NotNull(appProvider, nameof(appProvider));
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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0-rc2" />
<PackageReference Include="NodaTime" Version="2.2.4" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0" />
<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="StyleCop.Analyzers" Version="1.0.2" />
<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.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities
{
public abstract class SquidexDomainObjectGrain<T> : DomainObjectGrain<T> where T : IDomainState, new()
{
protected SquidexDomainObjectGrain(IStore<Guid> store)
: base(store)
protected SquidexDomainObjectGrain(IStore<Guid> store, ISemanticLog log)
: base(store, log)
{
}

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

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="GraphQL" Version="0.17.3" />
<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="StyleCop.Analyzers" Version="1.0.2" />
<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.Tasks;
using Microsoft.AspNetCore.Identity;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Tasks;
@ -72,11 +73,6 @@ namespace Squidex.Domain.Users.MongoDb
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)
{
return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken);
@ -388,5 +384,31 @@ namespace Squidex.Domain.Users.MongoDb
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" />
</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.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="StyleCop.Analyzers" Version="1.0.2" />
<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;
}
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.SetDisplayName(displayName);

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

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<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>
<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));
this.connection = connection;
this.position = projectionClient.ParsePositionOrNull(position);
this.subscriber = subscriber;
var streamName = projectionClient.CreateProjectionAsync(streamFilter).Result;
subscription = SubscribeToStream(streamName);
this.subscriber = subscriber;
this.subscription = SubscribeToStream(streamName);
}
public Task StopAsync()

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

@ -11,8 +11,8 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.4.1" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.4.4" />
<PackageReference Include="MongoDB.Driver" Version="2.5.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<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 Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities
namespace Squidex.Infrastructure
{
public abstract class CachingProviderBase
{

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

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
@ -19,6 +20,7 @@ namespace Squidex.Infrastructure.Commands
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private readonly IStore<Guid> store;
private readonly ISemanticLog log;
private Guid id;
private T snapshot = new T { Version = EtagVersion.Empty };
private IPersistence<T> persistence;
@ -43,20 +45,29 @@ namespace Squidex.Infrastructure.Commands
get { return snapshot; }
}
protected DomainObjectGrain(IStore<Guid> store)
protected DomainObjectGrain(IStore<Guid> store, ISemanticLog log)
{
Guard.NotNull(store, nameof(store));
Guard.NotNull(log, nameof(log));
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)
@ -162,7 +173,8 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectNotFoundException(id.ToString(), GetType());
}
else if (!isUpdate && Version >= 0)
if (!isUpdate && Version >= 0)
{
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)
{
if (context.Method == null)
if (context.InterfaceMethod == null)
{
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[0] != null)
{
@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.Commands
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()
{
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)

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

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

3
src/Squidex.Infrastructure/FileExtensions.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Globalization;
using System.IO;
namespace Squidex.Infrastructure
@ -62,7 +63,7 @@ namespace Squidex.Infrastructure
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.Collections.Generic;
using Microsoft.Extensions.Logging;
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)
{
writer.WriteException(exception);
@ -86,14 +103,5 @@ namespace Squidex.Infrastructure.Log.Adapter
{
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
{
private const int NumTries = 10;
private readonly IGrainFactory grainFactory;
public Bootstrap(IGrainFactory grainFactory)
@ -23,11 +24,26 @@ namespace Squidex.Infrastructure.Orleans
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 Orleans.CodeGeneration;
using Orleans.Serialization;
using Squidex.Infrastructure.Log;
namespace Squidex.Infrastructure.Orleans
{
public struct J<T>
{
private readonly T value;
public T Value
{
get { return value; }
}
public T Value { get; }
[JsonConstructor]
public J(T value)
{
this.value = value;
Value = value;
}
public static implicit operator T(J<T> value)
{
return value.value;
return value.Value;
}
public static implicit operator J<T>(T d)
@ -41,7 +37,7 @@ namespace Squidex.Infrastructure.Orleans
public override string ToString()
{
return value?.ToString() ?? string.Empty;
return Value?.ToString() ?? string.Empty;
}
public static Task<J<T>> AsTask(T value)
@ -58,32 +54,38 @@ namespace Squidex.Infrastructure.Orleans
[SerializerMethod]
public static void Serialize(object input, ISerializationContext context, Type expected)
{
var stream = new MemoryStream();
using (var writer = new JsonTextWriter(new StreamWriter(stream)))
using (Profile.Method(nameof(J)))
{
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);
context.StreamWriter.Write(outBytes);
var outBytes = stream.ToArray();
context.StreamWriter.Write(outBytes.Length);
context.StreamWriter.Write(outBytes);
}
}
[DeserializerMethod]
public static object Deserialize(Type expected, IDeserializationContext context)
{
var outLength = context.StreamReader.ReadInt();
var outBytes = context.StreamReader.ReadBytes(outLength);
using (Profile.Method(nameof(J)))
{
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)))
{
return J.Serializer.Deserialize(reader, expected);
using (var reader = new JsonTextReader(new StreamReader(stream)))
{
return J.Serializer.Deserialize(reader, expected);
}
}
}
}

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

@ -8,13 +8,14 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<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.Logging" Version="2.0.1" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0-rc2" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0-rc2" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.0.0-rc2" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.1" />
<PackageReference Include="NodaTime" Version="2.2.4" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NodaTime" Version="2.2.5" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0001" />
<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)
{
Guard.NotNull(key, nameof(key));
ThrowIfDisposed();
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 SquidexHidden = "urn:squidex:hidden";
public static readonly string Prefix = "urn:squidex:";
}
}

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

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

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

@ -5,12 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Shared.Users
{
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 Squidex.Infrastructure;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users
namespace Squidex.Shared.Users
{
public static class UserExtensions
{
@ -35,6 +34,11 @@ namespace Squidex.Domain.Users
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)
{
user.SetClaim(SquidexClaimTypes.SquidexConsent, "true");
@ -45,6 +49,11 @@ namespace Squidex.Domain.Users
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)
{
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
{
[Area("Api")]
[ApiModelValidation(false)]
public abstract class ApiController : Controller
{
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 NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps
@ -50,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)]
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();
@ -76,11 +74,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)]
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);
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);
}
@ -104,7 +102,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)]
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();
}

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

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
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.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps
@ -50,9 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)]
public IActionResult GetContributors(string app)
{
var contributors = App.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Permission = x.Value }).ToArray();
var response = new ContributorsDto { Contributors = contributors, MaxContributors = appPlansProvider.GetPlanForApp(App).MaxContributors };
var response = ContributorsDto.FromApp(App, appPlansProvider);
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="request">Contributor object that needs to be added to the app.</param>
/// <returns>
/// 204 => User assigned to app.
/// 200 => User assigned to app.
/// 400 => User is already assigned to the app or not found.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorAssignedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)]
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>

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

@ -6,17 +6,13 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Apps
@ -50,14 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)]
public IActionResult GetLanguages(string app)
{
var response = App.LanguagesConfig.OfType<LanguageConfig>().Select(x =>
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();
var response = AppLanguageDto.FromApp(App);
Response.Headers["ETag"] = App.Version.ToString();
@ -82,9 +71,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)]
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);
}
@ -106,7 +97,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(1)]
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();
}

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

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

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

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

@ -6,7 +6,9 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -17,5 +19,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
[Required]
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.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -20,17 +23,17 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
public string Id { get; set; }
/// <summary>
/// The new version of the entity.
/// </summary>
public long Version { get; set; }
/// <summary>
/// The permission level of the user.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public AppContributorPermission Permission { get; set; }
/// <summary>
/// The new version of the entity.
/// </summary>
public long Version { get; set; }
/// <summary>
/// Gets the current plan name.
/// </summary>
@ -40,5 +43,17 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Gets the next plan name.
/// </summary>
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 NodaTime;
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
{
@ -58,5 +61,17 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Gets the next plan name.
/// </summary>
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.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.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -40,5 +45,26 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Indicates if the language is optional.
/// </summary>
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.Collections.Generic;
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
{
@ -33,5 +37,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The regex message.
/// </summary>
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.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AssignAppContributorDto
{
/// <summary>
/// The id of the user to add to the app.
/// The id or email of the user to add to the app.
/// </summary>
[Required]
public string ContributorId { get; set; }
@ -25,5 +27,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
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.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -38,5 +41,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
[JsonConverter(typeof(StringEnumConverter))]
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.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -21,5 +24,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The maximum number of allowed contributors.
/// </summary>
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 Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -17,5 +19,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
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 Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
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.
/// </summary>
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.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -25,5 +27,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
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 Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -26,5 +28,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// Optional fallback languages.
/// </summary>
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.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -27,5 +30,15 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// The regex message.
/// </summary>
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.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
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, Request.QueryString.ToString());
var response = new AssetsDto
{
Total = assets.Total,
Items = assets.Select(x => SimpleMapper.Map(x, new AssetDto { FileType = x.FileName.FileType() })).ToArray()
};
var response = AssetsDto.FromAssets(assets);
Response.Headers["Surrogate-Key"] = string.Join(" ", response.Items.Select(x => x.Id));
@ -127,7 +122,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
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["Surrogate-Key"] = entity.Id.ToString();
@ -161,7 +156,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>();
var response = AssetCreatedDto.Create(command, result);
var response = AssetCreatedDto.FromCommand(command, result);
return StatusCode(201, response);
}
@ -217,9 +212,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request)
{
var command = SimpleMapper.Map(request, new RenameAsset { AssetId = id });
await CommandBus.PublishAsync(command);
await CommandBus.PublishAsync(request.ToCommand(id));
return NoContent();
}

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

@ -8,6 +8,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models
@ -67,13 +68,14 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
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
{
Id = command.AssetId,
FileName = command.File.FileName,
FileSize = command.File.FileSize,
FileType = command.File.FileName.FileType(),
FileVersion = result.Version,
MimeType = command.File.MimeType,
IsImage = command.ImageInfo != null,

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

@ -8,7 +8,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
@ -88,5 +90,10 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// The version of the asset.
/// </summary>
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.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
@ -16,5 +19,10 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
[Required]
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.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetsDto
{
/// <summary>
/// The total number of assets.
/// The assets.
/// </summary>
public long Total { get; set; }
[Required]
public AssetDto[] Items { get; set; }
/// <summary>
/// The assets.
/// The total number of assets.
/// </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.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline;
@ -57,7 +56,9 @@ namespace Squidex.Areas.Api.Controllers.Backups
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>

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

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

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

@ -15,7 +15,6 @@ using NodaTime.Text;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
@ -119,23 +118,8 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = new ContentsDto
{
Total = result.Contents.Total,
Items = result.Contents.Take(200).Select(item =>
{
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()
Total = result.Total,
Items = result.Take(200).Select(item => SimpleMapper.Map(item, new ContentDto { Data = item.Data })).ToArray()
};
Response.Headers["Surrogate-Key"] = string.Join(" ", response.Items.Select(x => x.Id));
@ -162,24 +146,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 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);
}
}
var response = SimpleMapper.Map(content, new ContentDto { Data = content.Data });
Response.Headers["ETag"] = entity.Version.ToString();
Response.Headers["Surrogate-Key"] = entity.Id.ToString();
@ -209,25 +178,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
public async Task<IActionResult> GetContentVersion(string app, string name, Guid id, int version)
{
var (schema, entity) = await contentQuery.FindContentAsync(App, name, User, id, version);
var response = SimpleMapper.Map(content, new ContentDto { Data = content.Data });
var response = SimpleMapper.Map(entity, new ContentDto());
if (entity.Data != null)
{
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"] = version.ToString();
Response.Headers["ETag"] = content.Version.ToString();
Response.Headers["Surrogate-Key"] = content.Id.ToString();
return Ok(response.Data);
}
@ -253,14 +208,14 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 context = await CommandBus.PublishAsync(command);
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);
}
@ -287,10 +242,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>();
@ -321,10 +275,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 context = await CommandBus.PublishAsync(command);
var result = context.Result<ContentDataChangedResult>();
@ -354,7 +307,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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);
@ -384,7 +337,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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);
@ -414,7 +367,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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);
@ -444,7 +397,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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);
@ -501,7 +454,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)]
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 };

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; }
/// <summary>
/// Gets the status of the content.
/// The the status of the content.
/// </summary>
public Status Status { get; set; }
@ -80,7 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// </summary>
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();

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

@ -13,7 +13,6 @@ using Orleans;
using Squidex.Areas.Api.Controllers.EventConsumers.Models;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.EventConsumers
@ -39,9 +38,9 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
{
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]

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

@ -5,6 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
{
public sealed class EventConsumerDto
@ -18,5 +21,10 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers.Models
public string Error { 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.Domain.Apps.Entities.History.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
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 response = entities.Select(x => SimpleMapper.Map(x, new HistoryEventDto())).ToList();
var response = entities.Select(HistoryEventDto.FromHistoryEvent).ToList();
return Ok(response);
}

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

@ -8,6 +8,8 @@
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Entities.History;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.History.Models
{
@ -45,5 +47,10 @@ namespace Squidex.Areas.Api.Controllers.History.Models
/// The version identifier.
/// </summary>
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 Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers
{
@ -22,5 +24,10 @@ namespace Squidex.Areas.Api.Controllers
/// </summary>
[Required]
public string EnglishName { get; set; }
public static LanguageDto FromLanguage(Language language)
{
return SimpleMapper.Map(language, new LanguageDto());
}
}
}

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

Loading…
Cancel
Save