Browse Source

Merge branch 'release/4.x'

# Conflicts:
#	backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs
pull/590/head
Sebastian 5 years ago
parent
commit
917d4639b1
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_it.json
  3. 1
      backend/i18n/frontend_nl.json
  4. 4
      backend/i18n/source/backend__ignore.json
  5. 4
      backend/i18n/source/frontend__ignore.json
  6. 1
      backend/i18n/source/frontend_en.json
  7. 2
      backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
  10. 14
      backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  11. 15
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  12. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  13. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs
  15. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  16. 14
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  17. 2
      backend/src/Squidex.Infrastructure.Amazon/Squidex.Infrastructure.Amazon.csproj
  18. 79
      backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs
  19. 28
      backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs
  20. 22
      backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs
  21. 18
      backend/src/Squidex.Infrastructure/Caching/IPubSub.cs
  22. 13
      backend/src/Squidex.Infrastructure/Caching/IPubSubSubscription.cs
  23. 20
      backend/src/Squidex.Infrastructure/Caching/IReplicatedCache.cs
  24. 123
      backend/src/Squidex.Infrastructure/Caching/LRUCache.cs
  25. 19
      backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs
  26. 36
      backend/src/Squidex.Infrastructure/Caching/LocalCacheExtensions.cs
  27. 103
      backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs
  28. 14
      backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs
  29. 32
      backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs
  30. 2
      backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs
  31. 2
      backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs
  32. 19
      backend/src/Squidex.Infrastructure/Orleans/IPubSubGrain.cs
  33. 19
      backend/src/Squidex.Infrastructure/Orleans/IPubSubGrainObserver.cs
  34. 2
      backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs
  35. 79
      backend/src/Squidex.Infrastructure/Orleans/OrleansPubSub.cs
  36. 42
      backend/src/Squidex.Infrastructure/Orleans/OrleansPubSubGrain.cs
  37. 3
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  38. 11
      backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs
  39. 2
      backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs
  40. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs
  41. 14
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  42. 2
      backend/src/Squidex/Config/Orleans/OrleansServices.cs
  43. 3
      backend/src/Squidex/Squidex.csproj
  44. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs
  45. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  46. 40
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  47. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  48. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs
  49. 123
      backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs
  50. 98
      backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs
  51. 141
      backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs
  52. 82
      backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs
  53. 14
      frontend/app/features/content/pages/content/content-field.component.ts
  54. 3
      frontend/app/features/content/pages/content/content-section.component.html
  55. 13
      frontend/app/features/content/pages/content/content-section.component.scss
  56. 3
      frontend/app/features/content/pages/content/content-section.component.ts
  57. 1
      frontend/app/features/content/shared/references/content-creator.component.html
  58. 4
      frontend/app/features/content/shared/references/content-creator.component.scss
  59. 4
      frontend/app/features/content/shared/references/content-selector.component.scss
  60. 15
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html
  61. 3
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.scss
  62. 2
      frontend/app/features/settings/pages/roles/role.component.html
  63. 1
      frontend/app/shared/services/schemas.types.ts
  64. 1
      frontend/app/shared/state/schemas.forms.ts
  65. 9
      frontend/app/theme/_forms.scss

1
backend/i18n/frontend_en.json

@ -662,6 +662,7 @@
"schemas.field.empty": "No field created yet.", "schemas.field.empty": "No field created yet.",
"schemas.field.enable": "Enable in UI", "schemas.field.enable": "Enable in UI",
"schemas.field.enabledMarker": "Enabled", "schemas.field.enabledMarker": "Enabled",
"schemas.field.halfWidth": "Half Width",
"schemas.field.hiddenMarker": "Hidden", "schemas.field.hiddenMarker": "Hidden",
"schemas.field.hide": "Hide in API", "schemas.field.hide": "Hide in API",
"schemas.field.hintsHint": "Describe this schema for documentation and user interfaces.", "schemas.field.hintsHint": "Describe this schema for documentation and user interfaces.",

1
backend/i18n/frontend_it.json

@ -662,6 +662,7 @@
"schemas.field.empty": "Nessun campo è stato ancora creato.", "schemas.field.empty": "Nessun campo è stato ancora creato.",
"schemas.field.enable": "Abilita nella UI", "schemas.field.enable": "Abilita nella UI",
"schemas.field.enabledMarker": "Abilitato", "schemas.field.enabledMarker": "Abilitato",
"schemas.field.halfWidth": "Half Width",
"schemas.field.hiddenMarker": "Nasconsto", "schemas.field.hiddenMarker": "Nasconsto",
"schemas.field.hide": "Nascondi nelle API", "schemas.field.hide": "Nascondi nelle API",
"schemas.field.hintsHint": "Descrivi questo schema per la documentazione e le interfacce utente.", "schemas.field.hintsHint": "Descrivi questo schema per la documentazione e le interfacce utente.",

1
backend/i18n/frontend_nl.json

@ -662,6 +662,7 @@
"schemas.field.empty": "Nog geen veld aangemaakt.", "schemas.field.empty": "Nog geen veld aangemaakt.",
"schemas.field.enable": "Inschakelen in gebruikersinterface", "schemas.field.enable": "Inschakelen in gebruikersinterface",
"schemas.field.enabledMarker": "Ingeschakeld", "schemas.field.enabledMarker": "Ingeschakeld",
"schemas.field.halfWidth": "Half Width",
"schemas.field.hiddenMarker": "Verborgen", "schemas.field.hiddenMarker": "Verborgen",
"schemas.field.hide": "Verbergen in API", "schemas.field.hide": "Verbergen in API",
"schemas.field.hintsHint": "Beschrijf dit schema voor documentatie en gebruikersinterfaces.", "schemas.field.hintsHint": "Beschrijf dit schema voor documentatie en gebruikersinterfaces.",

4
backend/i18n/source/backend__ignore.json

@ -151,10 +151,10 @@
"/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs": [ "/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs": [
"*" "*"
], ],
"/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager_Impl.cs": [ "/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager.cs": [
"*" "*"
], ],
"/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager.cs": [ "/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/IndexManager_Impl.cs": [
"*" "*"
], ],
"/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs": [ "/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs": [

4
backend/i18n/source/frontend__ignore.json

@ -19,8 +19,8 @@
"#{{index + 1}}" "#{{index + 1}}"
], ],
"/features/content/shared/forms/field-editor.component.html": [ "/features/content/shared/forms/field-editor.component.html": [
"{{field.displayName}} {{displaySuffix}}", "*",
"*" "{{field.displayName}} {{displaySuffix}}"
], ],
"/features/content/shared/references/references-editor.component.html": [ "/features/content/shared/references/references-editor.component.html": [
"·" "·"

1
backend/i18n/source/frontend_en.json

@ -662,6 +662,7 @@
"schemas.field.empty": "No field created yet.", "schemas.field.empty": "No field created yet.",
"schemas.field.enable": "Enable in UI", "schemas.field.enable": "Enable in UI",
"schemas.field.enabledMarker": "Enabled", "schemas.field.enabledMarker": "Enabled",
"schemas.field.halfWidth": "Half Width",
"schemas.field.hiddenMarker": "Hidden", "schemas.field.hiddenMarker": "Hidden",
"schemas.field.hide": "Hide in API", "schemas.field.hide": "Hide in API",
"schemas.field.hintsHint": "Describe this schema for documentation and user interfaces.", "schemas.field.hintsHint": "Describe this schema for documentation and user interfaces.",

2
backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs

@ -7,9 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow; using System.Threading.Tasks.Dataflow;
using MongoDB.Bson; using MongoDB.Bson;

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs

@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
public bool IsRequired { get; set; } public bool IsRequired { get; set; }
public bool IsHalfWidth { get; set; }
public string? Placeholder { get; set; } public string? Placeholder { get; set; }
public string? EditorUrl { get; set; } public string? EditorUrl { get; set; }

2
backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -8,6 +8,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Caching;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
@ -15,7 +16,6 @@ using Squidex.Domain.Apps.Entities.Rules.Indexes;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities

14
backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs

@ -10,9 +10,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
@ -36,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Guard.NotNull(replicatedCache, nameof(replicatedCache)); Guard.NotNull(replicatedCache, nameof(replicatedCache));
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
this.replicatedCache = replicatedCache; this.replicatedCache = replicatedCache;
} }
@ -149,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
if (app != null) if (app != null)
{ {
CacheIt(app, false); await CacheItAsync(app, false);
} }
return app; return app;
@ -227,7 +226,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
if (app != null) if (app != null)
{ {
CacheIt(app, true); await CacheItAsync(app, true);
switch (context.Command) switch (context.Command)
{ {
@ -319,10 +318,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
return $"APPS_NAME_{name}"; return $"APPS_NAME_{name}";
} }
private void CacheIt(IAppEntity app, bool publish) private Task CacheItAsync(IAppEntity app, bool publish)
{ {
replicatedCache.Add(GetCacheKey(app.Id), app, CacheDuration, publish); return Task.WhenAll(
replicatedCache.Add(GetCacheKey(app.Name), app, CacheDuration, publish); replicatedCache.AddAsync(GetCacheKey(app.Id), app, CacheDuration, publish),
replicatedCache.AddAsync(GetCacheKey(app.Name), app, CacheDuration, publish));
} }
} }
} }

15
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!c.DoNotScript && c.Publish) if (!c.DoNotScript && c.Publish)
{ {
await context.ExecuteScriptAsync(s => s.Change, c.Data = await context.ExecuteScriptAndTransformAsync(s => s.Change,
new ScriptVars new ScriptVars
{ {
Operation = "Published", Operation = "Published",
@ -198,14 +198,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!c.DoNotScript) if (!c.DoNotScript)
{ {
await context.ExecuteScriptAsync(s => s.Change, var data = Snapshot.Data.Clone();
var newData = await context.ExecuteScriptAndTransformAsync(s => s.Change,
new ScriptVars new ScriptVars
{ {
Operation = change.ToString(), Operation = change.ToString(),
Data = Snapshot.Data, Data = data,
Status = c.Status, Status = c.Status,
StatusOld = Snapshot.EditingStatus StatusOld = Snapshot.EditingStatus
}); });
if (!newData.Equals(Snapshot.Data))
{
var command = SimpleMapper.Map(c, new UpdateContent { Data = newData });
Update(command, newData);
}
} }
ChangeStatus(c, change); ChangeStatus(c, change);

9
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -15,20 +15,21 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService public sealed class CachingGraphQLService : IGraphQLService
{ {
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IMemoryCache cache;
private readonly IServiceProvider resolver; private readonly IServiceProvider resolver;
public CachingGraphQLService(IMemoryCache cache, IServiceProvider resolver) public CachingGraphQLService(IMemoryCache cache, IServiceProvider resolver)
: base(cache)
{ {
Guard.NotNull(cache, nameof(cache));
Guard.NotNull(resolver, nameof(resolver)); Guard.NotNull(resolver, nameof(resolver));
this.cache = cache;
this.resolver = resolver; this.resolver = resolver;
} }
@ -83,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); var cacheKey = CreateCacheKey(app.Id, app.Version.ToString());
return Cache.GetOrCreateAsync(cacheKey, async entry => return cache.GetOrCreateAsync(cacheKey, async entry =>
{ {
entry.AbsoluteExpirationRelativeToNow = CacheDuration; entry.AbsoluteExpirationRelativeToNow = CacheDuration;

12
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
@ -33,20 +32,21 @@ using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public class ContentQueryParser : CachingProviderBase public class ContentQueryParser
{ {
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly IMemoryCache cache;
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly ContentOptions options; private readonly ContentOptions options;
public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions<ContentOptions> options) public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions<ContentOptions> options)
: base(cache)
{ {
Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
Guard.NotNull(cache, nameof(cache));
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
this.jsonSerializer = jsonSerializer; this.jsonSerializer = jsonSerializer;
this.cache = cache;
this.options = options.Value; this.options = options.Value;
} }
@ -138,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient); var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient);
var result = Cache.GetOrCreate(cacheKey, entry => var result = cache.GetOrCreate(cacheKey, entry =>
{ {
entry.AbsoluteExpirationRelativeToNow = CacheTime; entry.AbsoluteExpirationRelativeToNow = CacheTime;
@ -152,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient); var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient);
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry => var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry =>
{ {
entry.AbsoluteExpirationRelativeToNow = CacheTime; entry.AbsoluteExpirationRelativeToNow = CacheTime;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs

@ -8,8 +8,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Caching;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Domain.Apps.Entities.Contents.Text.State namespace Squidex.Domain.Apps.Entities.Contents.Text.State
{ {

2
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -9,12 +9,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Rules namespace Squidex.Domain.Apps.Entities.Rules

14
backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs

@ -10,9 +10,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
@ -33,7 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
Guard.NotNull(replicatedCache, nameof(replicatedCache)); Guard.NotNull(replicatedCache, nameof(replicatedCache));
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
this.replicatedCache = replicatedCache; this.replicatedCache = replicatedCache;
} }
@ -99,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
if (schema != null) if (schema != null)
{ {
CacheIt(schema, false); await CacheItAsync(schema, false);
} }
return schema; return schema;
@ -159,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
if (schema != null) if (schema != null)
{ {
CacheIt(schema, true); await CacheItAsync(schema, true);
if (context.Command is DeleteSchema) if (context.Command is DeleteSchema)
{ {
@ -221,10 +220,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
return $"SCHEMAS_ID_{appId}_{id}"; return $"SCHEMAS_ID_{appId}_{id}";
} }
private void CacheIt(ISchemaEntity schema, bool publish) private Task CacheItAsync(ISchemaEntity schema, bool publish)
{ {
replicatedCache.Add(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration, publish); return Task.WhenAll(
replicatedCache.Add(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration, publish); replicatedCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration, publish),
replicatedCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration, publish));
} }
} }
} }

2
backend/src/Squidex.Infrastructure.Amazon/Squidex.Infrastructure.Amazon.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.5.3.1" /> <PackageReference Include="AWSSDK.S3" Version="3.5.3.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup> </ItemGroup>

79
backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs

@ -1,79 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Threading;
using Squidex.Infrastructure.Tasks;
#pragma warning disable CS8601 // Possible null reference assignment.
namespace Squidex.Infrastructure.Caching
{
public sealed class AsyncLocalCache : ILocalCache
{
private static readonly AsyncLocal<ConcurrentDictionary<object, object>> LocalCache = new AsyncLocal<ConcurrentDictionary<object, object>>();
private static readonly AsyncLocalCleaner<ConcurrentDictionary<object, object>> Cleaner;
static AsyncLocalCache()
{
Cleaner = new AsyncLocalCleaner<ConcurrentDictionary<object, object>>(LocalCache);
}
public IDisposable StartContext()
{
LocalCache.Value = new ConcurrentDictionary<object, object>();
return Cleaner;
}
public void Add(object key, object? value)
{
var cacheKey = GetCacheKey(key);
var cache = LocalCache.Value;
if (cache != null)
{
cache[cacheKey] = value;
}
}
public void Remove(object key)
{
var cacheKey = GetCacheKey(key);
var cache = LocalCache.Value;
if (cache != null)
{
cache.TryRemove(cacheKey, out _);
}
}
public bool TryGetValue(object key, out object? value)
{
var cacheKey = GetCacheKey(key);
var cache = LocalCache.Value;
if (cache != null)
{
return cache.TryGetValue(cacheKey, out value);
}
value = null;
return false;
}
private static string GetCacheKey(object key)
{
return $"CACHE_{key}";
}
}
}

28
backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs

@ -1,28 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Caching.Memory;
namespace Squidex.Infrastructure.Caching
{
public abstract class CachingProviderBase
{
private readonly IMemoryCache cache;
protected IMemoryCache Cache
{
get { return cache; }
}
protected CachingProviderBase(IMemoryCache cache)
{
Guard.NotNull(cache, nameof(cache));
this.cache = cache;
}
}
}

22
backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs

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

18
backend/src/Squidex.Infrastructure/Caching/IPubSub.cs

@ -1,18 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Caching
{
public interface IPubSub
{
void Publish(object message);
void Subscribe(Action<object> handler);
}
}

13
backend/src/Squidex.Infrastructure/Caching/IPubSubSubscription.cs

@ -1,13 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Caching
{
internal interface IPubSubSubscription
{
}
}

20
backend/src/Squidex.Infrastructure/Caching/IReplicatedCache.cs

@ -1,20 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Caching
{
public interface IReplicatedCache
{
void Add(string key, object? value, TimeSpan expiration, bool invalidate);
void Remove(string key);
bool TryGetValue(string key, out object? value);
}
}

123
backend/src/Squidex.Infrastructure/Caching/LRUCache.cs

@ -1,123 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Caching
{
public sealed class LRUCache<TKey, TValue> where TKey : notnull
{
private readonly Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>> cacheMap = new Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>>();
private readonly LinkedList<LRUCacheItem<TKey, TValue>> cacheHistory = new LinkedList<LRUCacheItem<TKey, TValue>>();
private readonly int capacity;
private readonly Action<TKey, TValue> itemEvicted;
public int Count
{
get { return cacheMap.Count; }
}
public IEnumerable<TKey> Keys
{
get { return cacheMap.Keys; }
}
public LRUCache(int capacity, Action<TKey, TValue>? itemEvicted = null)
{
Guard.GreaterThan(capacity, 0, nameof(capacity));
this.capacity = capacity;
this.itemEvicted = itemEvicted ?? ((key, value) => { });
}
public void Clear()
{
cacheHistory.Clear();
cacheMap.Clear();
}
public bool Set(TKey key, TValue value)
{
if (cacheMap.TryGetValue(key, out var node))
{
node.Value.Value = value;
cacheHistory.Remove(node);
cacheHistory.AddLast(node);
cacheMap[key] = node;
return true;
}
if (cacheMap.Count >= capacity)
{
RemoveFirst();
}
var cacheItem = new LRUCacheItem<TKey, TValue> { Key = key, Value = value };
node = new LinkedListNode<LRUCacheItem<TKey, TValue>>(cacheItem);
cacheMap.Add(key, node);
cacheHistory.AddLast(node);
return false;
}
public bool Remove(TKey key)
{
if (cacheMap.TryGetValue(key, out var node))
{
cacheMap.Remove(key);
cacheHistory.Remove(node);
return true;
}
return false;
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
value = default!;
if (cacheMap.TryGetValue(key, out var node))
{
value = node.Value.Value;
cacheHistory.Remove(node);
cacheHistory.AddLast(node);
return true;
}
return false;
}
public bool Contains(TKey key)
{
return cacheMap.ContainsKey(key);
}
private void RemoveFirst()
{
var node = cacheHistory.First;
if (node != null)
{
itemEvicted(node.Value.Key, node.Value.Value);
cacheMap.Remove(node.Value.Key);
cacheHistory.RemoveFirst();
}
}
}
}

19
backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
#pragma warning disable SA1401 // Fields must be private
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
namespace Squidex.Infrastructure.Caching
{
internal class LRUCacheItem<TKey, TValue>
{
public TKey Key;
public TValue Value;
}
}

36
backend/src/Squidex.Infrastructure/Caching/LocalCacheExtensions.cs

@ -1,36 +0,0 @@
// ==========================================================================
// 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 LocalCacheExtensions
{
public static async Task<T> GetOrCreateAsync<T>(this ILocalCache cache, object key, Func<Task<T>> task)
{
if (cache.TryGetValue(key, out var value))
{
if (value is T typed)
{
return typed;
}
else
{
return default!;
}
}
var result = await task();
cache.Add(key, result);
return result;
}
}
}

103
backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs

@ -1,103 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace Squidex.Infrastructure.Caching
{
public sealed class ReplicatedCache : IReplicatedCache
{
private readonly Guid instanceId = Guid.NewGuid();
private readonly IMemoryCache memoryCache;
private readonly IPubSub pubSub;
private readonly ReplicatedCacheOptions options;
public class InvalidateMessage
{
public Guid Source { get; set; }
public string Key { get; set; }
}
public ReplicatedCache(IMemoryCache memoryCache, IPubSub pubSub, IOptions<ReplicatedCacheOptions> options)
{
Guard.NotNull(memoryCache, nameof(memoryCache));
Guard.NotNull(pubSub, nameof(pubSub));
Guard.NotNull(options, nameof(options));
this.memoryCache = memoryCache;
this.pubSub = pubSub;
if (options.Value.Enable)
{
this.pubSub.Subscribe(OnMessage);
}
this.options = options.Value;
}
private void OnMessage(object message)
{
if (message is InvalidateMessage invalidate && invalidate.Source != instanceId)
{
memoryCache.Remove(invalidate.Key);
}
}
public void Add(string key, object? value, TimeSpan expiration, bool invalidate)
{
if (!options.Enable)
{
return;
}
memoryCache.Set(key, value, expiration);
if (invalidate)
{
Invalidate(key);
}
}
public void Remove(string key)
{
if (!options.Enable)
{
return;
}
memoryCache.Remove(key);
Invalidate(key);
}
public bool TryGetValue(string key, out object? value)
{
if (!options.Enable)
{
value = null;
return false;
}
return memoryCache.TryGetValue(key, out value);
}
private void Invalidate(string key)
{
if (!options.Enable)
{
return;
}
pubSub.Publish(new InvalidateMessage { Key = key, Source = instanceId });
}
}
}

14
backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs

@ -1,14 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Caching
{
public sealed class ReplicatedCacheOptions
{
public bool Enable { get; set; }
}
}

32
backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs

@ -1,32 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Infrastructure.Caching
{
public class SimplePubSub : IPubSub
{
private readonly List<Action<object>> handlers = new List<Action<object>>();
public virtual void Publish(object message)
{
foreach (var handler in handlers)
{
handler(message);
}
}
public virtual void Subscribe(Action<object> handler)
{
Guard.NotNull(handler, nameof(handler));
handlers.Add(handler);
}
}
}

2
backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs

@ -10,7 +10,7 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow; using System.Threading.Tasks.Dataflow;
using Squidex.Infrastructure.Caching; using Squidex.Caching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;

2
backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs

@ -8,7 +8,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading; using System.Threading;
using Squidex.Infrastructure.Caching; using Squidex.Caching;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans
{ {

19
backend/src/Squidex.Infrastructure/Orleans/IPubSubGrain.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Infrastructure.Orleans
{
public interface IPubSubGrain : IGrainWithStringKey
{
Task SubscribeAsync(IPubSubGrainObserver observer);
Task PublishAsync(object message);
}
}

19
backend/src/Squidex.Infrastructure/Orleans/IPubSubGrainObserver.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Orleans;
namespace Squidex.Infrastructure.Orleans
{
public interface IPubSubGrainObserver : IGrainObserver
{
void Handle(object message);
void Subscribe(Action<object> handler);
}
}

2
backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs

@ -8,7 +8,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Infrastructure.Caching; using Squidex.Caching;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans
{ {

79
backend/src/Squidex.Infrastructure/Orleans/OrleansPubSub.cs

@ -1,79 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Orleans
{
public sealed class OrleansPubSub : IBackgroundProcess, IPubSub
{
private readonly IPubSubGrain pubSubGrain;
private readonly IPubSubGrainObserver pubSubGrainObserver = new Observer();
private readonly IGrainFactory grainFactory;
private sealed class Observer : IPubSubGrainObserver
{
private readonly List<Action<object>> subscriptions = new List<Action<object>>();
public void Handle(object message)
{
foreach (var subscription in subscriptions)
{
try
{
subscription(message);
}
catch
{
continue;
}
}
}
public void Subscribe(Action<object> handler)
{
subscriptions.Add(handler);
}
}
public OrleansPubSub(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
pubSubGrain = grainFactory.GetGrain<IPubSubGrain>(SingleGrain.Id);
}
public async Task StartAsync(CancellationToken ct)
{
var reference = await grainFactory.CreateObjectReference<IPubSubGrainObserver>(pubSubGrainObserver);
await pubSubGrain.SubscribeAsync(reference);
}
public void Publish(object message)
{
Guard.NotNull(message, nameof(message));
pubSubGrain.PublishAsync(message).Forget();
}
public void Subscribe(Action<object> handler)
{
Guard.NotNull(handler, nameof(handler));
pubSubGrainObserver.Subscribe(handler);
}
}
}

42
backend/src/Squidex.Infrastructure/Orleans/OrleansPubSubGrain.cs

@ -1,42 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Infrastructure.Orleans
{
public sealed class OrleansPubSubGrain : Grain, IPubSubGrain
{
private readonly List<IPubSubGrainObserver> subscriptions = new List<IPubSubGrainObserver>();
public Task PublishAsync(object message)
{
foreach (var subscription in subscriptions)
{
try
{
subscription.Handle(message);
}
catch
{
continue;
}
}
return Task.CompletedTask;
}
public Task SubscribeAsync(IPubSubGrainObserver observer)
{
subscriptions.Add(observer);
return Task.CompletedTask;
}
}
}

3
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -15,7 +15,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" /> <PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="FluentFTP" Version="32.4.5" /> <PackageReference Include="FluentFTP" Version="32.4.6" />
<PackageReference Include="Fody" Version="6.2.6"> <PackageReference Include="Fody" Version="6.2.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -37,6 +37,7 @@
<PackageReference Include="NodaTime" Version="3.0.1" /> <PackageReference Include="NodaTime" Version="3.0.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
<PackageReference Include="Squidex.Caching" Version="1.1.0" />
<PackageReference Include="Squidex.Text" Version="1.1.0" /> <PackageReference Include="Squidex.Text" Version="1.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" /> <PackageReference Include="System.Collections.Immutable" Version="1.7.1" />

11
backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs

@ -9,21 +9,22 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure.Caching;
namespace Squidex.Infrastructure.UsageTracking namespace Squidex.Infrastructure.UsageTracking
{ {
public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker public sealed class CachingUsageTracker : IUsageTracker
{ {
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
private readonly IUsageTracker inner; private readonly IUsageTracker inner;
private readonly IMemoryCache cache;
public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache) public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache)
: base(cache)
{ {
Guard.NotNull(inner, nameof(inner)); Guard.NotNull(inner, nameof(inner));
Guard.NotNull(cache, nameof(cache));
this.inner = inner; this.inner = inner;
this.cache = cache;
} }
public Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate) public Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate)
@ -46,7 +47,7 @@ namespace Squidex.Infrastructure.UsageTracking
var cacheKey = string.Join("$", "Usage", nameof(GetForMonthAsync), key, date, category); var cacheKey = string.Join("$", "Usage", nameof(GetForMonthAsync), key, date, category);
return Cache.GetOrCreateAsync(cacheKey, entry => return cache.GetOrCreateAsync(cacheKey, entry =>
{ {
entry.AbsoluteExpirationRelativeToNow = CacheDuration; entry.AbsoluteExpirationRelativeToNow = CacheDuration;
@ -60,7 +61,7 @@ namespace Squidex.Infrastructure.UsageTracking
var cacheKey = string.Join("$", "Usage", nameof(GetAsync), key, fromDate, toDate, category); var cacheKey = string.Join("$", "Usage", nameof(GetAsync), key, fromDate, toDate, category);
return Cache.GetOrCreateAsync(cacheKey, entry => return cache.GetOrCreateAsync(cacheKey, entry =>
{ {
entry.AbsoluteExpirationRelativeToNow = CacheDuration; entry.AbsoluteExpirationRelativeToNow = CacheDuration;

2
backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs

@ -7,8 +7,8 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Squidex.Caching;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldPropertiesDto.cs

@ -43,6 +43,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// </summary> /// </summary>
public bool IsRequired { get; set; } public bool IsRequired { get; set; }
/// <summary>
/// Indicates if the field should be rendered with half width only.
/// </summary>
public bool IsHalfWidth { get; set; }
/// <summary> /// <summary>
/// Optional url to the editor. /// Optional url to the editor.
/// </summary> /// </summary>

14
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -15,6 +15,7 @@ using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.News; using Squidex.Areas.Api.Controllers.News;
using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Areas.Api.Controllers.News.Service;
using Squidex.Areas.Api.Controllers.UI; using Squidex.Areas.Api.Controllers.UI;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions; using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
@ -24,7 +25,6 @@ using Squidex.Domain.Apps.Entities.Contents.Counter;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
@ -49,6 +49,9 @@ namespace Squidex.Config.Domain
services.Configure<ReplicatedCacheOptions>( services.Configure<ReplicatedCacheOptions>(
config.GetSection("caching:replicated")); config.GetSection("caching:replicated"));
services.AddReplicatedCache();
services.AddAsyncLocalCache();
services.AddSingletonAs(_ => SystemClock.Instance) services.AddSingletonAs(_ => SystemClock.Instance)
.As<IClock>(); .As<IClock>();
@ -58,15 +61,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<GrainTagService>() services.AddSingletonAs<GrainTagService>()
.As<ITagService>(); .As<ITagService>();
services.AddSingletonAs<AsyncLocalCache>()
.As<ILocalCache>();
services.AddSingletonAs<ReplicatedCache>()
.As<IReplicatedCache>();
services.AddSingletonAs<OrleansPubSub>()
.As<IPubSub>();
services.AddSingletonAs<JintScriptEngine>() services.AddSingletonAs<JintScriptEngine>()
.AsOptional<IScriptEngine>(); .AsOptional<IScriptEngine>();

2
backend/src/Squidex/Config/Orleans/OrleansServices.cs

@ -28,6 +28,8 @@ namespace Squidex.Config.Orleans
{ {
public static void ConfigureForSquidex(this ISiloBuilder builder, IConfiguration config) public static void ConfigureForSquidex(this ISiloBuilder builder, IConfiguration config)
{ {
builder.AddOrleansPubSub();
builder.ConfigureServices(siloServices => builder.ConfigureServices(siloServices =>
{ {
siloServices.AddSingleton<IMongoClientFactory, DefaultMongoClientFactory>(); siloServices.AddSingleton<IMongoClientFactory, DefaultMongoClientFactory>();

3
backend/src/Squidex/Squidex.csproj

@ -58,7 +58,8 @@
<PackageReference Include="Orleans.Providers.MongoDB" Version="3.1.7" /> <PackageReference Include="Orleans.Providers.MongoDB" Version="3.1.7" />
<PackageReference Include="OrleansDashboard" Version="3.1.0" /> <PackageReference Include="OrleansDashboard" Version="3.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.7.0" PrivateAssets="all" /> <PackageReference Include="ReportGenerator" Version="4.7.1" PrivateAssets="all" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.1.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="5.5.0" /> <PackageReference Include="Squidex.ClientLibrary" Version="5.5.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" /> <PackageReference Include="System.Linq" Version="4.3.0" />

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs

@ -9,12 +9,13 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Orleans; using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
.Returns(indexByUser); .Returns(indexByUser);
var cache = var cache =
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(), new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake<ILogger<SimplePubSub>>()),
Options.Create(new ReplicatedCacheOptions { Enable = true })); Options.Create(new ReplicatedCacheOptions { Enable = true }));
sut = new AppsIndex(grainFactory, cache); sut = new AppsIndex(grainFactory, cache);

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs

@ -11,6 +11,7 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
@ -20,7 +21,6 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Xunit; using Xunit;

40
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -165,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft), "<create-script>", ScriptOptions())) A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft), "<create-script>", ScriptOptions()))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Published), "<change-script>", ScriptOptions())) A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Published), "<change-script>", ScriptOptions()))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -375,7 +375,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Published, Status.Draft), "<change-script>", ScriptOptions())) A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Published, Status.Draft), "<change-script>", ScriptOptions()))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -397,7 +397,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions())) A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions()))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -420,7 +420,35 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions())) A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions()))
.MustHaveHappened();
}
[Fact]
public async Task ChangeStatus_should_also_update_when_script_changes_data()
{
var command = new ChangeContentStatus { Status = Status.Draft };
A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions()))
.Returns(otherData);
await ExecuteCreateAsync();
await ExecutePublishAsync();
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(Status.Draft, sut.Snapshot.CurrentVersion.Status);
Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = otherData }),
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions()))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -444,7 +472,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived }) CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions())) A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions()))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -499,7 +527,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions())) A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, "<change-script>", ScriptOptions()))
.MustHaveHappened(); .MustHaveHappened();
} }

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -12,13 +12,13 @@ using FakeItEasy;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NodaTime; using NodaTime;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Events.Contents; using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Xunit; using Xunit;

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs

@ -9,12 +9,13 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Orleans; using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
@ -39,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
.Returns(index); .Returns(index);
var cache = var cache =
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(), new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake<ILogger<SimplePubSub>>()),
Options.Create(new ReplicatedCacheOptions { Enable = true })); Options.Create(new ReplicatedCacheOptions { Enable = true }));
sut = new SchemasIndex(grainFactory, cache); sut = new SchemasIndex(grainFactory, cache);

123
backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs

@ -1,123 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class AsyncLocalCacheTests
{
private readonly ILocalCache sut = new AsyncLocalCache();
private int called;
[Fact]
public async Task Should_add_item_to_cache_when_context_exists()
{
using (sut.StartContext())
{
sut.Add("Key", 1);
await Task.Delay(5);
AssertCache(sut, "Key", 1, true);
await Task.Delay(5);
sut.Remove("Key");
AssertCache(sut, "Key", null, false);
}
}
[Fact]
public async Task Should_not_add_item_to_cache_when_context_not_exists()
{
sut.Add("Key", 1);
await Task.Delay(5);
AssertCache(sut, "Key", null, false);
sut.Remove("Key");
await Task.Delay(5);
AssertCache(sut, "Key", null, false);
}
[Fact]
public async Task Should_call_creator_once_when_context_exists()
{
using (sut.StartContext())
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
}
[Fact]
public async Task Should_call_creator_twice_when_context_not_exists()
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
[Fact]
public async Task Should_call_async_creator_once_when_context_exists()
{
using (sut.StartContext())
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
}
[Fact]
public async Task Should_call_async_creator_twice_when_context_not_exists()
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
private static void AssertCache(ILocalCache cache, string key, object? expectedValue, bool expectedFound)
{
var found = cache.TryGetValue(key, out var value);
Assert.Equal(expectedFound, found);
Assert.Equal(expectedValue, value);
}
}
}

98
backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs

@ -1,98 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class LRUCacheTests
{
private readonly LRUCache<string, int> sut = new LRUCache<string, int>(10);
private readonly string key = "Key";
[Fact]
public void Should_always_override_when_setting_value()
{
sut.Set(key, 1);
sut.Set(key, 2);
Assert.True(sut.TryGetValue(key, out var value));
Assert.True(sut.Contains(key));
Assert.Equal(2, value);
}
[Fact]
public void Should_clear_items()
{
sut.Set("1", 1);
sut.Set("2", 2);
Assert.Equal(2, sut.Count);
sut.Clear();
Assert.Equal(0, sut.Count);
}
[Fact]
public void Should_remove_old_items_whentC_capacity_reached()
{
for (var i = 0; i < 15; i++)
{
sut.Set(i.ToString(), i);
}
for (var i = 0; i < 5; i++)
{
Assert.False(sut.TryGetValue(i.ToString(), out var value));
Assert.Equal(0, value);
}
for (var i = 5; i < 15; i++)
{
Assert.True(sut.TryGetValue(i.ToString(), out var value));
Assert.Equal(i, value);
}
}
[Fact]
public void Should_notify_about_evicted_items()
{
var evicted = new List<int>();
var cache = new LRUCache<int, int>(3, (key, _) => evicted.Add(key));
cache.Set(1, 1);
cache.Set(2, 2);
cache.Set(3, 3);
cache.Set(1, 1);
cache.Set(4, 4);
cache.Set(5, 5);
Assert.Equal(new List<int> { 2, 3 }, evicted);
}
[Fact]
public void Should_return_false_when_item_to_remove_does_not_exist()
{
Assert.False(sut.Remove(key));
}
[Fact]
public void Should_remove_inserted_item()
{
sut.Set(key, 2);
Assert.True(sut.Remove(key));
Assert.False(sut.Contains(key));
Assert.False(sut.TryGetValue(key, out var value));
Assert.Equal(0, value);
}
}
}

141
backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs

@ -1,141 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class ReplicatedCacheTests
{
private readonly IPubSub pubSub = A.Fake<SimplePubSub>(options => options.CallsBaseMethods());
private readonly ReplicatedCacheOptions options = new ReplicatedCacheOptions { Enable = true };
private readonly ReplicatedCache sut;
public ReplicatedCacheTests()
{
sut = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
}
[Fact]
public void Should_serve_from_cache()
{
sut.Add("Key", 1, TimeSpan.FromMinutes(10), true);
AssertCache(sut, "Key", 1, true);
sut.Remove("Key");
AssertCache(sut, "Key", null, false);
}
[Fact]
public void Should_not_serve_from_cache_disabled()
{
options.Enable = false;
sut.Add("Key", 1, TimeSpan.FromMilliseconds(100), true);
AssertCache(sut, "Key", null, false);
}
[Fact]
public async Task Should_not_serve_from_cache_when_expired()
{
sut.Add("Key", 1, TimeSpan.FromMilliseconds(1), true);
await Task.Delay(100);
AssertCache(sut, "Key", null, false);
}
[Fact]
public void Should_not_invalidate_other_instances_when_item_added_and_flag_is_false()
{
var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
cache1.Add("Key", 1, TimeSpan.FromMinutes(1), false);
cache2.Add("Key", 2, TimeSpan.FromMinutes(1), false);
AssertCache(cache1, "Key", 1, true);
AssertCache(cache2, "Key", 2, true);
}
[Fact]
public void Should_invalidate_other_instances_when_item_added_and_flag_is_true()
{
var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
cache1.Add("Key", 1, TimeSpan.FromMinutes(1), true);
cache2.Add("Key", 2, TimeSpan.FromMinutes(1), true);
AssertCache(cache1, "Key", null, false);
AssertCache(cache2, "Key", 2, true);
}
[Fact]
public void Should_invalidate_other_instances_when_item_removed()
{
var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
cache1.Add("Key", 1, TimeSpan.FromMinutes(1), true);
cache2.Remove("Key");
AssertCache(cache1, "Key", null, false);
AssertCache(cache2, "Key", null, false);
}
[Fact]
public void Should_send_invalidation_message_when_added_and_flag_is_true()
{
sut.Add("Key", 1, TimeSpan.FromMinutes(1), true);
A.CallTo(() => pubSub.Publish(A<object>._))
.MustHaveHappened();
}
[Fact]
public void Should_not_send_invalidation_message_when_added_flag_is_false()
{
sut.Add("Key", 1, TimeSpan.FromMinutes(1), false);
A.CallTo(() => pubSub.Publish(A<object>._))
.MustNotHaveHappened();
}
[Fact]
public void Should_not_send_invalidation_message_when_added_but_disabled()
{
options.Enable = false;
sut.Add("Key", 1, TimeSpan.FromMinutes(1), true);
A.CallTo(() => pubSub.Publish(A<object>._))
.MustNotHaveHappened();
}
private static void AssertCache(IReplicatedCache cache, string key, object? expectedValue, bool expectedFound)
{
var found = cache.TryGetValue(key, out var value);
Assert.Equal(expectedFound, found);
Assert.Equal(expectedValue, value);
}
private static MemoryCache CreateMemoryCache()
{
return new MemoryCache(Options.Create(new MemoryCacheOptions()));
}
}
}

82
backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs

@ -1,82 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
using Orleans.TestingHost;
using Xunit;
namespace Squidex.Infrastructure.Orleans
{
[Trait("Category", "Dependencies")]
public class PubSubTests
{
[Fact]
public async Task Simple_pubsub_tests()
{
var cluster =
new TestClusterBuilder(3)
.Build();
await cluster.DeployAsync();
var sent = new HashSet<Guid>
{
Guid.NewGuid(),
Guid.NewGuid(),
Guid.NewGuid()
};
var received1 = await CreateSubscriber(cluster.Client, sent.Count);
var received2 = await CreateSubscriber(cluster.Client, sent.Count);
var pubSub = new OrleansPubSub(cluster.Client);
foreach (var message in sent)
{
pubSub.Publish(message);
}
await Task.WhenAny(
Task.WhenAll(
received1,
received2
),
Task.Delay(10000));
Assert.True(received1.Result.SetEquals(sent));
Assert.True(received2.Result.SetEquals(sent));
}
private static async Task<Task<HashSet<Guid>>> CreateSubscriber(IGrainFactory grainFactory, int expectedCount)
{
var pubSub = new OrleansPubSub(grainFactory);
await pubSub.StartAsync(default);
var received = new HashSet<Guid>();
var receivedCompleted = new TaskCompletionSource<HashSet<Guid>>();
pubSub.Subscribe(message =>
{
if (message is Guid guid)
{
received.Add(guid);
}
if (received.Count == expectedCount)
{
receivedCompleted.TrySetResult(received);
}
});
return receivedCompleted.Task;
}
}
}

14
frontend/app/features/content/pages/content/content-field.component.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared'; import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -19,6 +19,9 @@ export class ContentFieldComponent implements OnChanges {
@Output() @Output()
public languageChange = new EventEmitter<AppLanguageDto>(); public languageChange = new EventEmitter<AppLanguageDto>();
@Input()
public compact = false;
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;
@ -43,6 +46,15 @@ export class ContentFieldComponent implements OnChanges {
@Input() @Input()
public languages: ReadonlyArray<AppLanguageDto>; public languages: ReadonlyArray<AppLanguageDto>;
@HostBinding('class')
public get class() {
return this.isHalfWidth ? 'col-6 half-field' : 'col-12';
}
public get isHalfWidth() {
return this.formModel.field.properties.isHalfWidth && !this.compact && !this.formCompare;
}
public showAllControls = false; public showAllControls = false;
public isDifferent: Observable<boolean>; public isDifferent: Observable<boolean>;

3
frontend/app/features/content/pages/content/content-section.component.html

@ -17,9 +17,10 @@
</div> </div>
</ng-container> </ng-container>
<div [class.hidden]="isCollapsed && !formCompare"> <div class="row small-gutters" [class.hidden]="isCollapsed && !formCompare">
<sqx-content-field *ngFor="let field of formSection.fields; trackBy: trackByField" <sqx-content-field *ngFor="let field of formSection.fields; trackBy: trackByField"
(languageChange)="languageChange.emit($event)" (languageChange)="languageChange.emit($event)"
[compact]="compact"
[form]="form" [form]="form"
[formCompare]="formCompare" [formCompare]="formCompare"
[formContext]="formContext" [formContext]="formContext"

13
frontend/app/features/content/pages/content/content-section.component.scss

@ -1,3 +1,16 @@
:host ::ng-deep {
.small-gutters {
margin-left: .25rem;
margin-right: .25rem;
> .col-6,
> .col-12 {
padding-left: .25rem;
padding-right: .25rem;
}
}
}
.btn { .btn {
& { & {
width: 2rem; width: 2rem;

3
frontend/app/features/content/pages/content/content-section.component.ts

@ -18,6 +18,9 @@ export class ContentSectionComponent implements OnChanges {
@Output() @Output()
public languageChange = new EventEmitter<AppLanguageDto>(); public languageChange = new EventEmitter<AppLanguageDto>();
@Input()
public compact = false;
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;

1
frontend/app/features/content/shared/references/content-creator.component.html

@ -38,6 +38,7 @@
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()"> <form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-content-section *ngFor="let section of contentForm.sections" <sqx-content-section *ngFor="let section of contentForm.sections"
[(language)]="language" [(language)]="language"
[compact]="true"
[form]="contentForm" [form]="contentForm"
[formContext]="contentFormContext" [formContext]="contentFormContext"
[formSection]="section" [formSection]="section"

4
frontend/app/features/content/shared/references/content-creator.component.scss

@ -8,4 +8,8 @@
.modal-content { .modal-content {
background: $color-background; background: $color-background;
} }
.form-control-dark {
border: 1px solid darken($color-modal-header-foreground, 20%);
}
} }

4
frontend/app/features/content/shared/references/content-selector.component.scss

@ -6,6 +6,10 @@
.modal-tabs { .modal-tabs {
background: $color-dark-foreground; background: $color-dark-foreground;
} }
.form-control-dark {
border: 1px solid darken($color-modal-header-foreground, 20%);
}
} }
.col-selector { .col-selector {

15
frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html

@ -1,5 +1,5 @@
<div [formGroup]="fieldForm"> <div [formGroup]="fieldForm">
<div class="form-group row"> <div class="form-group row mb-2">
<label class="col-3 col-form-label" for="{{field.fieldId}}_editorUrl">{{ 'schemas.field.editorUrl' | sqxTranslate }}</label> <label class="col-3 col-form-label" for="{{field.fieldId}}_editorUrl">{{ 'schemas.field.editorUrl' | sqxTranslate }}</label>
<div class="col-6"> <div class="col-6">
@ -41,3 +41,16 @@
<sqx-tags-ui [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-tags-ui> <sqx-tags-ui [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-tags-ui>
</ng-container> </ng-container>
</ng-container> </ng-container>
<div [formGroup]="fieldForm">
<div class="form-group row mt-2">
<div class="col-9 offset-3">
<div class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" id="{{field.fieldId}}_fieldHalfWidth" formControlName="isHalfWidth">
<label class="custom-control-label" for="{{field.fieldId}}_fieldHalfWidth">
{{ 'schemas.field.halfWidth' | sqxTranslate }}
</label>
</div>
</div>
</div>
</div>

3
frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.scss

@ -1,3 +0,0 @@
.form-group {
margin-bottom: 1rem;
}

2
frontend/app/features/settings/pages/roles/role.component.html

@ -69,7 +69,7 @@
{{descriptions[role.name] | sqxTranslate}} {{descriptions[role.name] | sqxTranslate}}
</sqx-form-alert> </sqx-form-alert>
<table class="table table-bordered table-fixed table-sm"> <table class="table table-bordered table-fixed">
<tr *ngFor="let control of editForm.controls; let i = index"> <tr *ngFor="let control of editForm.controls; let i = index">
<td> <td>
{{control.value}} {{control.value}}

1
frontend/app/shared/services/schemas.types.ts

@ -135,6 +135,7 @@ export abstract class FieldPropertiesDto {
public readonly editorUrl?: string; public readonly editorUrl?: string;
public readonly hints?: string; public readonly hints?: string;
public readonly isRequired: boolean = false; public readonly isRequired: boolean = false;
public readonly isHalfWidth: boolean = false;
public readonly label?: string; public readonly label?: string;
public readonly placeholder?: string; public readonly placeholder?: string;
public readonly tags?: ReadonlyArray<string>; public readonly tags?: ReadonlyArray<string>;

1
frontend/app/shared/state/schemas.forms.ts

@ -214,6 +214,7 @@ export class EditFieldForm extends Form<FormGroup, {}, FieldPropertiesDto> {
], ],
editorUrl: null, editorUrl: null,
isRequired: false, isRequired: false,
isHalfWidth: false,
tags: [] tags: []
})); }));
} }

9
frontend/app/theme/_forms.scss

@ -260,11 +260,14 @@ label {
} }
} }
// sass-lint:disable quotes
$select-indicator: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'><path fill='#ccc' d='M2 0L0 2h4zm0 5L0 3h4z'/></svg>") !default;;
// Dark form control for the dark panel. // Dark form control for the dark panel.
.form-control-dark { .form-control-dark {
& { & {
@include placeholder-color($color-dark2-placeholder); @include placeholder-color($color-dark2-placeholder);
background: $color-dark2-control; background-color: $color-dark2-control;
border: 1px solid $color-dark2-control; border: 1px solid $color-dark2-control;
color: darken($color-dark-foreground, 20%); color: darken($color-dark-foreground, 20%);
transition: background-color .3s ease; transition: background-color .3s ease;
@ -275,6 +278,10 @@ label {
border-color: lighten($color-dark2-control, 2%); border-color: lighten($color-dark2-control, 2%);
color: $color-dark2-focus-foreground; color: $color-dark2-focus-foreground;
} }
&.custom-select {
background: $color-dark2-control escape-svg($select-indicator) no-repeat right .75rem center / 8px 10px;
}
} }
input { input {

Loading…
Cancel
Save