Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/468/head
Sebastian 6 years ago
parent
commit
cb23a8ad5a
  1. 1
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs
  2. 1
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs
  3. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  4. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  6. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  7. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  8. 110
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs
  9. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs
  10. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  11. 31
      backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs
  12. 48
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
  13. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  14. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
  15. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  16. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs
  17. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  18. 25
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs
  19. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
  20. 25
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs
  21. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
  22. 70
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  23. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  24. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs
  25. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  26. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  27. 9
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs
  28. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs
  29. 31
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  31. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  32. 11
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs
  33. 13
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  34. 19
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  35. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  36. 30
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs
  37. 25
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs
  38. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoIndexOutput.cs
  39. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs
  40. 9
      backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  41. 225
      backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  42. 36
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  43. 9
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
  44. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs
  45. 10
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  46. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs
  47. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  48. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  49. 222
      backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs
  50. 25
      backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs
  51. 17
      backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  52. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs
  53. 20
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs
  54. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs
  55. 69
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs
  56. 39
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs
  57. 43
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  58. 34
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  59. 23
      backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs
  60. 50
      backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  61. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  62. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  63. 19
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  64. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  65. 53
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  66. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs
  67. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  68. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  69. 61
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  70. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs
  71. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  72. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  73. 2
      backend/src/Squidex.Domain.Apps.Entities/DomainEntityExtensions.cs
  74. 43
      backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs
  75. 82
      backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs
  76. 21
      backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs
  77. 16
      backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs
  78. 16
      backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs
  79. 17
      backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs
  80. 6
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  81. 15
      backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs
  82. 34
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  83. 52
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs
  84. 42
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  85. 15
      backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  86. 1
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  87. 3
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs
  88. 9
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs
  89. 11
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs
  90. 12
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs
  91. 1
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs
  92. 17
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  93. 136
      backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs
  94. 17
      backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  95. 9
      backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  96. 17
      backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs
  97. 3
      backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs
  98. 3
      backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs
  99. 18
      backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs
  100. 8
      backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs

1
backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;

1
backend/extensions/Squidex.Extensions/Actions/Notification/NotificationPlugin.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Plugins;

10
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNullOrEmpty(id);
return new AppClients(Without(id));
return Without<AppClients>(id);
}
[Pure]
@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.Apps
throw new ArgumentException("Id already exists.", nameof(id));
}
return new AppClients(With(id, client));
return With<AppClients>(id, client);
}
[Pure]
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Core.Apps
throw new ArgumentException("Id already exists.", nameof(id));
}
return new AppClients(With(id, new AppClient(id, secret, Role.Editor)));
return With<AppClients>(id, new AppClient(id, secret, Role.Editor));
}
[Pure]
@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return new AppClients(With(id, client.Rename(newName)));
return With<AppClients>(id, client.Rename(newName), DeepComparer<AppClient>.Instance);
}
[Pure]
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return new AppClients(With(id, client.Update(role)));
return With<AppClients>(id, client.Update(role), DeepComparer<AppClient>.Instance);
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Apps
Guard.NotNullOrEmpty(contributorId);
Guard.NotNullOrEmpty(role);
return new AppContributors(With(contributorId, role));
return With<AppContributors>(contributorId, role, EqualityComparer<string>.Default);
}
[Pure]
@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNullOrEmpty(contributorId);
return new AppContributors(Without(contributorId));
return Without<AppContributors>(contributorId);
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Apps
[Pure]
public AppPatterns Remove(Guid id)
{
return new AppPatterns(Without(id));
return Without<AppPatterns>(id);
}
[Pure]
@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.Apps
throw new ArgumentException("Id already exists.", nameof(id));
}
return new AppPatterns(With(id, newPattern));
return With<AppPatterns>(id, newPattern);
}
[Pure]
@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return new AppPatterns(With(id, appPattern.Update(name, pattern, message)));
return With<AppPatterns>(id, appPattern.Update(name, pattern, message), DeepComparer<AppPattern>.Instance);
}
}
}

17
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNull(language);
return new LanguagesConfig(languages, languages[language]);
return Create(languages, languages[language]);
}
[Pure]
@ -109,12 +109,11 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNull(config);
var newLanguages =
new ArrayDictionary<Language, LanguageConfig>(languages.With(config.Language, config));
var newLanguages = languages.With(config.Language, config);
var newMaster = Master?.Language == config.Language ? config : Master;
return new LanguagesConfig(newLanguages, newMaster!);
return Create(newLanguages, newMaster!);
}
[Pure]
@ -134,6 +133,16 @@ namespace Squidex.Domain.Apps.Core.Apps
newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ??
newLanguages.Values.FirstOrDefault();
return Create(newLanguages, newMaster);
}
private LanguagesConfig Create(ArrayDictionary<Language, LanguageConfig> newLanguages, LanguageConfig newMaster)
{
if (newLanguages.EqualsDictionary(languages, EqualityComparer<Language>.Default, DeepComparer<LanguageConfig>.Instance) && newMaster.Language.Equals(master.Language))
{
return this;
}
return new LanguagesConfig(newLanguages, newMaster);
}

11
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Apps
[Pure]
public Roles Remove(string name)
{
return new Roles(inner.Without(name));
return Create(inner.Without(name));
}
[Pure]
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return new Roles(inner.With(name, newRole));
return Create(inner.With(name, newRole));
}
[Pure]
@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return new Roles(inner.With(name, role.Update(permissions)));
return Create(inner.With(name, role.Update(permissions), DeepComparer<Role>.Instance));
}
public static bool IsDefault(string role)
@ -176,5 +176,10 @@ namespace Squidex.Domain.Apps.Core.Apps
{
return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray();
}
private Roles Create(ArrayDictionary<string, Role> newRoles)
{
return ReferenceEquals(inner, newRoles) ? this : new Roles(newRoles);
}
}
}

110
backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs

@ -0,0 +1,110 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Assets
{
public sealed class AssetMetadata : Dictionary<string, IJsonValue>
{
private static readonly char[] PathSeparators = { '.', '[', ']' };
public AssetMetadata SetPixelWidth(int value)
{
this["pixelWidth"] = JsonValue.Create(value);
return this;
}
public AssetMetadata SetPixelHeight(int value)
{
this["pixelHeight"] = JsonValue.Create(value);
return this;
}
public int? GetPixelWidth()
{
if (TryGetValue("pixelWidth", out var n) && n is JsonNumber number)
{
return (int)number.Value;
}
return null;
}
public int? GetPixelHeight()
{
if (TryGetValue("pixelHeight", out var n) && n is JsonNumber number)
{
return (int)number.Value;
}
return null;
}
public bool TryGetNumber(string name, out double result)
{
if (TryGetValue(name, out var v) && v is JsonNumber n)
{
result = n.Value;
return true;
}
result = 0;
return false;
}
public bool TryGetString(string name, [MaybeNullWhen(false)] out string result)
{
if (TryGetValue(name, out var v) && v is JsonString s)
{
result = s.Value;
return true;
}
result = null!;
return false;
}
public bool TryGetByPath(string? path, [MaybeNullWhen(false)] out object result)
{
return TryGetByPath(path?.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries), out result!);
}
public bool TryGetByPath(IEnumerable<string>? path, [MaybeNullWhen(false)] out object result)
{
result = this;
if (path == null || !path.Any())
{
return false;
}
result = null!;
if (!TryGetValue(path.First(), out var json))
{
return false;
}
json.TryGetByPath(path.Skip(1), out var temp);
result = temp!;
return true;
}
}
}

11
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs → backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetType.cs

@ -1,14 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities
namespace Squidex.Domain.Apps.Core.Assets
{
public interface IUpdateableEntityWithVersion
public enum AssetType
{
long Version { get; set; }
Unknown,
Image,
Audio,
Video
}
}

10
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Contents
[Pure]
public Workflows Remove(Guid id)
{
return new Workflows(Without(id));
return Without<Workflows>(id);
}
[Pure]
@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
Guard.NotNullOrEmpty(name);
return new Workflows(With(workflowId, Workflow.CreateDefault(name)));
return With<Workflows>(workflowId, Workflow.CreateDefault(name));
}
[Pure]
@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
Guard.NotNull(workflow);
return new Workflows(With(Guid.Empty, workflow));
return With<Workflows>(Guid.Empty, workflow, DeepComparer<Workflow>.Instance);
}
[Pure]
@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
Guard.NotNull(workflow);
return new Workflows(With(id, workflow));
return With<Workflows>(id, workflow, DeepComparer<Workflow>.Instance);
}
[Pure]
@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Core.Contents
return this;
}
return new Workflows(With(id, workflow));
return With<Workflows>(id, workflow, DeepComparer<Workflow>.Instance);
}
public Workflow GetFirst()

31
backend/src/Squidex.Domain.Apps.Core.Model/DeepComparer.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core
{
public sealed class DeepComparer<T> : IEqualityComparer<T>
{
public static readonly DeepComparer<T> Instance = new DeepComparer<T>();
private DeepComparer()
{
}
public bool Equals(T x, T y)
{
return x.IsDeepEqual(y);
}
public int GetHashCode(T obj)
{
return 0;
}
}
}

48
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs

@ -43,16 +43,18 @@ namespace Squidex.Domain.Apps.Core.Rules
Guard.NotNull(trigger);
Guard.NotNull(action);
this.trigger = trigger;
this.trigger.Freeze();
this.action = action;
this.action.Freeze();
SetTrigger(trigger);
SetAction(action);
}
[Pure]
public Rule Rename(string newName)
{
if (string.Equals(name, newName))
{
return this;
}
return Clone(clone =>
{
clone.name = newName;
@ -62,6 +64,11 @@ namespace Squidex.Domain.Apps.Core.Rules
[Pure]
public Rule Enable()
{
if (isEnabled)
{
return this;
}
return Clone(clone =>
{
clone.isEnabled = true;
@ -71,6 +78,11 @@ namespace Squidex.Domain.Apps.Core.Rules
[Pure]
public Rule Disable()
{
if (!isEnabled)
{
return this;
}
return Clone(clone =>
{
clone.isEnabled = false;
@ -87,11 +99,14 @@ namespace Squidex.Domain.Apps.Core.Rules
throw new ArgumentException("New trigger has another type.", nameof(newTrigger));
}
newTrigger.Freeze();
if (trigger.DeepEquals(newTrigger))
{
return this;
}
return Clone(clone =>
{
clone.trigger = newTrigger;
clone.SetTrigger(newTrigger);
});
}
@ -105,12 +120,27 @@ namespace Squidex.Domain.Apps.Core.Rules
throw new ArgumentException("New action has another type.", nameof(newAction));
}
newAction.Freeze();
if (action.DeepEquals(newAction))
{
return this;
}
return Clone(clone =>
{
clone.action = newAction;
clone.SetAction(newAction);
});
}
private void SetAction(RuleAction newAction)
{
action = newAction;
action.Freeze();
}
private void SetTrigger(RuleTrigger newTrigger)
{
trigger = newTrigger;
trigger.Freeze();
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs

@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using DeepEqual.Syntax;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Rules
@ -37,5 +38,10 @@ namespace Squidex.Domain.Apps.Core.Rules
{
yield break;
}
public bool DeepEquals(RuleAction action)
{
return this.WithDeepEqual(action).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
}
}

7
backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs

@ -5,10 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Rules
{
public abstract class RuleTrigger : Freezable
{
public abstract T Accept<T>(IRuleTriggerVisitor<T> visitor);
public bool DeepEquals(RuleTrigger action)
{
return this.WithDeepEqual(action).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
}
}

5
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs

@ -113,6 +113,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
throw new ArgumentException("Ids must cover all fields.", nameof(ids));
}
if (ids.SequenceEqual(fieldsOrdered.Select(x => x.Id)))
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray();

6
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs

@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas
{
@ -44,5 +45,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
return new FieldNames(list);
}
public bool DeepEquals(FieldNames names)
{
return this.IsDeepEqual(names);
}
}
}

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

@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.ObjectModel;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas
{
@ -26,5 +27,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
public abstract RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null);
public abstract NestedField CreateNestedField(long id, string name, IFieldSettings? settings = null);
public bool DeepEquals(FieldProperties properties)
{
return this.WithDeepEqual(properties).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
}
}

25
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs

@ -64,6 +64,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public NestedField Lock()
{
if (isLocked)
{
return this;
}
return Clone(clone =>
{
clone.isLocked = true;
@ -73,6 +78,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public NestedField Hide()
{
if (isHidden)
{
return this;
}
return Clone(clone =>
{
clone.isHidden = true;
@ -82,6 +92,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public NestedField Show()
{
if (!isHidden)
{
return this;
}
return Clone(clone =>
{
clone.isHidden = false;
@ -91,6 +106,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public NestedField Disable()
{
if (isDisabled)
{
return this;
}
return Clone(clone =>
{
clone.isDisabled = true;
@ -100,6 +120,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public NestedField Enable()
{
if (!isDisabled)
{
return this;
}
return Clone(clone =>
{
clone.isDisabled = false;

7
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs

@ -36,6 +36,13 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
var typedProperties = ValidateProperties(newProperties);
typedProperties.Freeze();
if (properties.DeepEquals(typedProperties))
{
return this;
}
return Clone<NestedField<T>>(clone =>
{
clone.SetProperties(typedProperties);

25
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs

@ -73,6 +73,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public RootField Lock()
{
if (isLocked)
{
return this;
}
return Clone(clone =>
{
clone.isLocked = true;
@ -82,6 +87,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public RootField Hide()
{
if (isHidden)
{
return this;
}
return Clone(clone =>
{
clone.isHidden = true;
@ -91,6 +101,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public RootField Show()
{
if (!isHidden)
{
return this;
}
return Clone(clone =>
{
clone.isHidden = false;
@ -100,6 +115,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public RootField Disable()
{
if (isDisabled)
{
return this;
}
return Clone(clone =>
{
clone.isDisabled = true;
@ -109,6 +129,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public RootField Enable()
{
if (!isDisabled)
{
return this;
}
return Clone(clone =>
{
clone.isDisabled = false;

5
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs

@ -36,6 +36,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
var typedProperties = ValidateProperties(newProperties);
if (properties.DeepEquals(typedProperties))
{
return this;
}
return Clone<RootField<T>>(clone =>
{
clone.SetProperties(typedProperties);

70
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

@ -116,21 +116,33 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public Schema Update(SchemaProperties newProperties)
{
Guard.NotNull(newProperties);
newProperties ??= new SchemaProperties();
if (properties.DeepEquals(newProperties))
{
return this;
}
return Clone(clone =>
{
clone.properties = newProperties;
clone.properties.Freeze();
clone.Properties.Freeze();
});
}
[Pure]
public Schema ConfigureScripts(SchemaScripts newScripts)
{
newScripts ??= new SchemaScripts();
if (scripts.DeepEquals(newScripts))
{
return this;
}
return Clone(clone =>
{
clone.scripts = newScripts ?? new SchemaScripts();
clone.scripts = newScripts;
clone.scripts.Freeze();
});
}
@ -138,42 +150,55 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public Schema ConfigureFieldsInLists(FieldNames names)
{
names ??= FieldNames.Empty;
if (fieldsInLists.DeepEquals(names))
{
return this;
}
return Clone(clone =>
{
clone.fieldsInLists = names ?? FieldNames.Empty;
clone.fieldsInLists = names;
});
}
[Pure]
public Schema ConfigureFieldsInLists(params string[] names)
{
return Clone(clone =>
{
clone.fieldsInLists = new FieldNames(names);
});
return ConfigureFieldsInLists(new FieldNames(names));
}
[Pure]
public Schema ConfigureFieldsInReferences(FieldNames names)
{
names ??= FieldNames.Empty;
if (fieldsInReferences.DeepEquals(names))
{
return this;
}
return Clone(clone =>
{
clone.fieldsInReferences = names ?? FieldNames.Empty;
clone.fieldsInReferences = names;
});
}
[Pure]
public Schema ConfigureFieldsInReferences(params string[] names)
{
return Clone(clone =>
{
clone.fieldsInReferences = new FieldNames(names);
});
return ConfigureFieldsInReferences(new FieldNames(names));
}
[Pure]
public Schema Publish()
{
if (isPublished)
{
return this;
}
return Clone(clone =>
{
clone.isPublished = true;
@ -183,6 +208,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public Schema Unpublish()
{
if (!isPublished)
{
return this;
}
return Clone(clone =>
{
clone.isPublished = false;
@ -192,6 +222,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public Schema ChangeCategory(string newCategory)
{
if (string.Equals(category, newCategory))
{
return this;
}
return Clone(clone =>
{
clone.category = newCategory;
@ -201,9 +236,16 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public Schema ConfigurePreviewUrls(IReadOnlyDictionary<string, string> newPreviewUrls)
{
previewUrls ??= EmptyPreviewUrls;
if (previewUrls.EqualsDictionary(newPreviewUrls))
{
return this;
}
return Clone(clone =>
{
clone.previewUrls = newPreviewUrls ?? EmptyPreviewUrls;
clone.previewUrls = newPreviewUrls;
});
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs

@ -6,11 +6,17 @@
// ==========================================================================
using System.Collections.ObjectModel;
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class SchemaProperties : NamedElementPropertiesBase
{
public ReadOnlyCollection<string> Tags { get; set; }
public bool DeepEquals(SchemaProperties properties)
{
return this.WithDeepEqual(properties).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
}
}

7
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using DeepEqual.Syntax;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class SchemaScripts : Freezable
@ -25,5 +27,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string Delete { get; set; }
public string Query { get; set; }
public bool DeepEquals(SchemaScripts scripts)
{
return this.WithDeepEqual(scripts).IgnoreProperty<Freezable>(x => x.IsFrozen).Compare();
}
}
}

1
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -10,6 +10,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Fody" Version="4.2.1" PrivateAssets="all" />
<PackageReference Include="Freezable.Fody" Version="1.9.3" PrivateAssets="all" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

15
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -13,17 +13,15 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.EventSynchronization
{
public static class SchemaSynchronizer
{
public static IEnumerable<IEvent> Synchronize(this Schema source, Schema? target, IJsonSerializer serializer, Func<long> idGenerator,
public static IEnumerable<IEvent> Synchronize(this Schema source, Schema? target, Func<long> idGenerator,
SchemaSynchronizationOptions? options = null)
{
Guard.NotNull(source);
Guard.NotNull(serializer);
Guard.NotNull(idGenerator);
if (target == null)
@ -39,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
return @event;
}
if (!source.Properties.EqualsJson(target.Properties, serializer))
if (!source.Properties.DeepEquals(target.Properties))
{
yield return E(new SchemaUpdated { Properties = target.Properties });
}
@ -49,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
yield return E(new SchemaCategoryChanged { Name = target.Category });
}
if (!source.Scripts.EqualsJson(target.Scripts, serializer))
if (!source.Scripts.DeepEquals(target.Scripts))
{
yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts });
}
@ -66,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
E(new SchemaUnpublished());
}
var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, CanUpdateRoot, null, options);
var events = SyncFields(source.FieldCollection, target.FieldCollection, idGenerator, CanUpdateRoot, null, options);
foreach (var @event in events)
{
@ -88,7 +86,6 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
private static IEnumerable<SchemaEvent> SyncFields<T>(
FieldCollection<T> source,
FieldCollection<T> target,
IJsonSerializer serializer,
Func<long> idGenerator,
Func<T, T, bool> canUpdate,
NamedId<long>? parentId, SchemaSynchronizationOptions options) where T : class, IField
@ -131,7 +128,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if (canUpdate(sourceField, targetField))
{
if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer))
if (!sourceField.RawProperties.DeepEquals(targetField.RawProperties))
{
yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties });
}
@ -194,7 +191,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{
var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, CanUpdate, id, options);
var events = SyncFields(fields, targetArrayField.FieldCollection, idGenerator, CanUpdate, id, options);
foreach (var @event in events)
{

9
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.EventSynchronization
{
@ -26,13 +25,5 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
{
return lhs.GetType() == rhs.GetType();
}
public static bool EqualsJson<T>(this T lhs, T rhs, IJsonSerializer serializer)
{
var lhsJson = serializer.Serialize(lhs);
var rhsJson = serializer.Serialize(rhs);
return string.Equals(lhsJson, rhsJson);
}
}
}

3
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs

@ -13,6 +13,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{
public sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference?>
{
private static readonly EdmComplexType JsonType = new EdmComplexType("Squidex", "Json", null, false, true);
private readonly EdmTypeFactory typeFactory;
internal EdmTypeVisitor(EdmTypeFactory typeFactory)
@ -67,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
public IEdmTypeReference? Visit(IField<JsonFieldProperties> field)
{
return null;
return new EdmComplexTypeReference(JsonType, !field.RawProperties.IsRequired);
}
public IEdmTypeReference? Visit(IField<NumberFieldProperties> field)

31
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs

@ -26,39 +26,52 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
return new JsonSchema { Type = JsonObjectType.String };
}
public static JsonSchemaProperty ArrayProperty(JsonSchema item)
public static JsonSchemaProperty ArrayProperty(JsonSchema item, string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item };
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Array, Item = item }, description, isRequired);
}
public static JsonSchemaProperty BooleanProperty()
public static JsonSchemaProperty BooleanProperty(string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.Boolean };
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Boolean }, description, isRequired);
}
public static JsonSchemaProperty DateTimeProperty(string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime, Description = description, IsRequired = isRequired };
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.DateTime }, description, isRequired);
}
public static JsonSchemaProperty GuidProperty(string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid, Description = description, IsRequired = isRequired };
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid }, description, isRequired);
}
public static JsonSchemaProperty NumberProperty(string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.Number, Description = description, IsRequired = isRequired };
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Number }, description, isRequired);
}
public static JsonSchemaProperty ObjectProperty(JsonSchema item, string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item, Description = description, IsRequired = isRequired };
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.Object, Reference = item }, description, isRequired);
}
public static JsonSchemaProperty StringProperty(string? description = null, bool isRequired = false)
{
return new JsonSchemaProperty { Type = JsonObjectType.String, Description = description, IsRequired = isRequired };
return Enrich(new JsonSchemaProperty { Type = JsonObjectType.String }, description, isRequired);
}
public static JsonSchemaProperty JsonProperty(string? description = null, bool isRequired = false)
{
return Enrich(new JsonSchemaProperty(), description, isRequired);
}
private static JsonSchemaProperty Enrich(JsonSchemaProperty property, string? description = null, bool isRequired = false)
{
property.Description = description;
property.IsRequired = isRequired;
return property;
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs

@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
public JsonSchemaProperty? Visit(IField<JsonFieldProperties> field)
{
return Builder.StringProperty();
return Builder.JsonProperty();
}
public JsonSchemaProperty? Visit(IField<NumberFieldProperties> field)

19
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Squidex.Domain.Apps.Core.Contents;
@ -286,28 +287,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return Fallback;
}
for (var j = 2; j < path.Length; j++)
if (path.Skip(2).Any())
{
if (value is JsonObject obj && obj.TryGetValue(path[j], out value))
{
continue;
}
if (value is JsonArray array && int.TryParse(path[j], out var idx) && idx >= 0 && idx < array.Count)
{
value = array[idx];
}
else
if (!value.TryGetByPath(path.Skip(2), out value) || value == null || value.Type == JsonValueType.Null)
{
return Fallback;
}
}
if (value == null || value.Type == JsonValueType.Null)
{
return Fallback;
}
return value.ToString() ?? Fallback;
}
}

11
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Assets;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
@ -15,16 +16,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
long FileSize { get; }
bool IsImage { get; }
int? PixelWidth { get; }
int? PixelHeight { get; }
string FileName { get; }
string FileHash { get; }
string Slug { get; }
AssetMetadata Metadata { get; }
AssetType Type { get; }
}
}

13
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
@ -61,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
addError(path, "Invalid file extension.");
}
if (!asset.IsImage)
if (asset.Type != AssetType.Image)
{
if (properties.MustBeImage)
{
@ -71,11 +72,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
continue;
}
if (asset.PixelWidth.HasValue &&
asset.PixelHeight.HasValue)
var pixelWidth = asset.Metadata.GetPixelWidth();
var pixelHeight = asset.Metadata.GetPixelHeight();
if (pixelWidth.HasValue && pixelHeight.HasValue)
{
var w = asset.PixelWidth.Value;
var h = asset.PixelHeight.Value;
var w = pixelWidth.Value;
var h = pixelHeight.Value;
var actualRatio = (double)w / h;

19
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -10,8 +10,10 @@ using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
@ -67,21 +69,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement("fv")]
public long FileVersion { get; set; }
[BsonRequired]
[BsonElement("im")]
public bool IsImage { get; set; }
[BsonRequired]
[BsonElement("vs")]
public long Version { get; set; }
[BsonRequired]
[BsonElement("pw")]
public int? PixelWidth { get; set; }
[BsonRequired]
[BsonElement("ph")]
public int? PixelHeight { get; set; }
[BsonElement("at")]
public AssetType Type { get; set; }
[BsonRequired]
[BsonElement("cb")]
@ -99,6 +93,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement("dl")]
public bool IsDeleted { get; set; }
[BsonJson]
[BsonRequired]
[BsonElement("md")]
public AssetMetadata Metadata { get; set; }
public Guid AssetId
{
get { return Id; }

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb.Queries;
using Squidex.Infrastructure.Queries;
@ -19,19 +18,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
public static class FindExtensions
{
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
private static readonly SortDefinitionBuilder<MongoAssetEntity> Sorting = Builders<MongoAssetEntity>.Sort;
public static ClrQuery AdjustToModel(this ClrQuery query)
{
if (query.Filter != null)
{
query.Filter = PascalCasePathConverter<ClrValue>.Transform(query.Filter);
query.Filter = FirstPascalPathConverter<ClrValue>.Transform(query.Filter);
}
query.Sort = query.Sort
.Select(x =>
new SortNode(
x.Path.Select(p => p.ToPascalCase()).ToList(),
x.Path.ToFirstPascalCase(),
x.Order))
.ToList();

30
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathConverter.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public sealed class FirstPascalPathConverter<TValue> : TransformVisitor<TValue>
{
private static readonly FirstPascalPathConverter<TValue> Instance = new FirstPascalPathConverter<TValue>();
private FirstPascalPathConverter()
{
}
public static FilterNode<TValue>? Transform(FilterNode<TValue> node)
{
return node.Accept(Instance);
}
public override FilterNode<TValue>? Visit(CompareFilter<TValue> nodeIn)
{
return new CompareFilter<TValue>(nodeIn.Path.ToFirstPascalCase(), nodeIn.Operator, nodeIn.Value);
}
}
}

25
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FirstPascalPathExtension.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
{
public static class FirstPascalPathExtension
{
public static PropertyPath ToFirstPascalCase(this PropertyPath path)
{
var result = path.ToList();
result[0] = result[0].ToPascalCase();
return new PropertyPath(result);
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoIndexOutput.cs

@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText
try
{
indexDirectory.Bucket.UploadFromStream(fullName, indexFileName, fs, options);
indexDirectory.Bucket.UploadFromStream(fullName, indexFileName, fs, options);
}
catch (MongoBulkWriteException ex) when (ex.WriteErrors.Any(x => x.Code == 11000))
{

6
backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardApp.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
@ -73,11 +72,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
e("Plan can only changed from the user who configured the plan initially.");
}
if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase))
{
e("App has already this plan.");
}
});
}
}

9
backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -52,14 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
throw new DomainForbiddenException("You cannot change your own role.");
}
if (contributors.TryGetValue(command.ContributorId, out var role))
{
if (role == command.Role)
{
e(Not.New("Contributor", "role"), nameof(command.Role));
}
}
else
if (!contributors.TryGetValue(command.ContributorId, out var role))
{
if (plan != null && plan.MaxContributors > 0 && contributors.Count >= plan.MaxContributors)
{

225
backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
@ -56,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
[DataMember]
public bool IsArchived { get; set; }
public void ApplyEvent(IEvent @event)
public override bool ApplyEvent(IEvent @event)
{
switch (@event)
{
@ -64,174 +65,91 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
{
SimpleMapper.Map(e, this);
break;
return true;
}
case AppUpdated e:
case AppUpdated e when !string.Equals(e.Label, Label) || !string.Equals(e.Description, Description):
{
SimpleMapper.Map(e, this);
break;
return true;
}
case AppImageUploaded e:
{
Image = e.Image;
break;
}
return UpdateImage(e, ev => ev.Image);
case AppImageRemoved _:
{
Image = null;
case AppImageRemoved e when Image != null:
return UpdateImage(e, ev => null);
break;
}
case AppPlanChanged e:
{
Plan = AppPlan.Build(e.Actor, e.PlanId);
case AppPlanChanged e when !string.Equals(Plan?.PlanId, e.PlanId):
return UpdatePlan(e, ev => AppPlan.Build(ev.Actor, ev.PlanId));
break;
}
case AppPlanReset _:
{
Plan = null;
break;
}
case AppPlanReset e when Plan != null:
return UpdatePlan(e, ev => null);
case AppContributorAssigned e:
{
Contributors = Contributors.Assign(e.ContributorId, e.Role);
break;
}
return UpdateContributors(e, (ev, c) => c.Assign(ev.ContributorId, ev.Role));
case AppContributorRemoved e:
{
Contributors = Contributors.Remove(e.ContributorId);
break;
}
return UpdateContributors(e, (ev, c) => c.Remove(ev.ContributorId));
case AppClientAttached e:
{
Clients = Clients.Add(e.Id, e.Secret);
break;
}
return UpdateClients(e, (ev, c) => c.Add(ev.Id, ev.Secret));
case AppClientUpdated e:
{
Clients = Clients.Update(e.Id, e.Role);
break;
}
return UpdateClients(e, (ev, c) => c.Update(ev.Id, ev.Role));
case AppClientRenamed e:
{
Clients = Clients.Rename(e.Id, e.Name);
break;
}
return UpdateClients(e, (ev, c) => c.Rename(ev.Id, ev.Name));
case AppClientRevoked e:
{
Clients = Clients.Revoke(e.Id);
break;
}
return UpdateClients(e, (ev, c) => c.Revoke(ev.Id));
case AppWorkflowAdded e:
{
Workflows = Workflows.Add(e.WorkflowId, e.Name);
break;
}
return UpdateWorkflows(e, (ev, w) => w.Add(ev.WorkflowId, ev.Name));
case AppWorkflowUpdated e:
{
Workflows = Workflows.Update(e.WorkflowId, e.Workflow);
break;
}
return UpdateWorkflows(e, (ev, w) => w.Update(ev.WorkflowId, ev.Workflow));
case AppWorkflowDeleted e:
{
Workflows = Workflows.Remove(e.WorkflowId);
break;
}
return UpdateWorkflows(e, (ev, w) => w.Remove(ev.WorkflowId));
case AppPatternAdded e:
{
Patterns = Patterns.Add(e.PatternId, e.Name, e.Pattern, e.Message);
break;
}
return UpdatePatterns(e, (ev, p) => p.Add(ev.PatternId, ev.Name, ev.Pattern, ev.Message));
case AppPatternDeleted e:
{
Patterns = Patterns.Remove(e.PatternId);
break;
}
return UpdatePatterns(e, (ev, p) => p.Remove(ev.PatternId));
case AppPatternUpdated e:
{
Patterns = Patterns.Update(e.PatternId, e.Name, e.Pattern, e.Message);
break;
}
return UpdatePatterns(e, (ev, p) => p.Update(ev.PatternId, ev.Name, ev.Pattern, ev.Message));
case AppRoleAdded e:
{
Roles = Roles.Add(e.Name);
break;
}
case AppRoleDeleted e:
{
Roles = Roles.Remove(e.Name);
break;
}
return UpdateRoles(e, (ev, r) => r.Add(ev.Name));
case AppRoleUpdated e:
{
Roles = Roles.Update(e.Name, e.Permissions);
return UpdateRoles(e, (ev, r) => r.Update(ev.Name, ev.Permissions));
break;
}
case AppRoleDeleted e:
return UpdateRoles(e, (ev, r) => r.Remove(ev.Name));
case AppLanguageAdded e:
{
LanguagesConfig = LanguagesConfig.Set(e.Language);
break;
}
return UpdateLanguages(e, (ev, l) => l.Set(ev.Language));
case AppLanguageRemoved e:
{
LanguagesConfig = LanguagesConfig.Remove(e.Language);
break;
}
return UpdateLanguages(e, (ev, l) => l.Remove(ev.Language));
case AppLanguageUpdated e:
return UpdateLanguages(e, (ev, l) =>
{
LanguagesConfig = LanguagesConfig.Set(e.Language, e.IsOptional, e.Fallback);
l = l.Set(ev.Language, ev.IsOptional, ev.Fallback);
if (e.IsMaster)
if (ev.IsMaster)
{
LanguagesConfig = LanguagesConfig.MakeMaster(e.Language);
LanguagesConfig = LanguagesConfig.MakeMaster(ev.Language);
}
break;
}
return l;
});
case AppArchived _:
{
@ -239,14 +157,79 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
IsArchived = true;
break;
return true;
}
}
return false;
}
private bool UpdateContributors<T>(T @event, Func<T, AppContributors, AppContributors> update)
{
var previous = Contributors;
Contributors = update(@event, previous);
return !ReferenceEquals(previous, Contributors);
}
private bool UpdateClients<T>(T @event, Func<T, AppClients, AppClients> update)
{
var previous = Clients;
Clients = update(@event, previous);
return !ReferenceEquals(previous, Clients);
}
public override AppState Apply(Envelope<IEvent> @event)
private bool UpdateLanguages<T>(T @event, Func<T, LanguagesConfig, LanguagesConfig> update)
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
var previous = LanguagesConfig;
LanguagesConfig = update(@event, previous);
return !ReferenceEquals(previous, LanguagesConfig);
}
private bool UpdatePatterns<T>(T @event, Func<T, AppPatterns, AppPatterns> update)
{
var previous = Patterns;
Patterns = update(@event, previous);
return !ReferenceEquals(previous, Patterns);
}
private bool UpdateRoles<T>(T @event, Func<T, Roles, Roles> update)
{
var previous = Roles;
Roles = update(@event, previous);
return !ReferenceEquals(previous, Roles);
}
private bool UpdateWorkflows<T>(T @event, Func<T, Workflows, Workflows> update)
{
var previous = Workflows;
Workflows = update(@event, previous);
return !ReferenceEquals(previous, Workflows);
}
private bool UpdateImage<T>(T @event, Func<T, AppImage?> update)
{
Image = update(@event);
return true;
}
private bool UpdatePlan<T>(T @event, Func<T, AppPlan?> update)
{
Plan = update(@event);
return true;
}
}
}

36
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -11,7 +11,6 @@ using System.Security.Cryptography;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
@ -23,33 +22,29 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetFileStore assetFileStore;
private readonly IAssetEnricher assetEnricher;
private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IContextProvider contextProvider;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
IAssetEnricher assetEnricher,
IAssetQueryService assetQuery,
IAssetFileStore assetFileStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IAssetQueryService assetQuery,
IContextProvider contextProvider,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
IEnumerable<IAssetMetadataSource> assetMetadataSources)
: base(grainFactory)
{
Guard.NotNull(assetEnricher);
Guard.NotNull(assetFileStore);
Guard.NotNull(assetQuery);
Guard.NotNull(assetThumbnailGenerator);
Guard.NotNull(assetMetadataSources);
Guard.NotNull(contextProvider);
Guard.NotNull(tagGenerators);
this.assetFileStore = assetFileStore;
this.assetEnricher = assetEnricher;
this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator;
this.contextProvider = contextProvider;
this.tagGenerators = tagGenerators;
this.assetMetadataSources = assetMetadataSources;
}
public override async Task HandleAsync(CommandContext context, Func<Task> next)
@ -60,7 +55,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
case CreateAsset createAsset:
{
await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, tempFile);
try
@ -82,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
}
GenerateTags(createAsset);
await EnrichWithMetadataAsync(createAsset, createAsset.Tags);
await HandleCoreAsync(context, next);
@ -102,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
case UpdateAsset updateAsset:
{
await EnrichWithImageInfosAsync(updateAsset);
await EnrichWithMetadataAsync(updateAsset);
await EnrichWithHashAndUploadAsync(updateAsset, tempFile);
try
@ -144,11 +138,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
return asset?.FileName == file.FileName && asset.FileSize == file.FileSize;
}
private async Task EnrichWithImageInfosAsync(UploadAssetCommand command)
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
}
private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile)
{
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
@ -159,16 +148,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
}
private void GenerateTags(CreateAsset createAsset)
private async Task EnrichWithMetadataAsync(UploadAssetCommand command, HashSet<string>? tags = null)
{
if (createAsset.Tags == null)
{
createAsset.Tags = new HashSet<string>();
}
foreach (var tagGenerator in tagGenerators)
foreach (var metadataSource in assetMetadataSources)
{
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
await metadataSource.EnhanceAsync(command, tags);
}
}
}

9
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets
@ -42,17 +43,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
public string Slug { get; set; }
public string MetadataText { get; set; }
public long FileSize { get; set; }
public long FileVersion { get; set; }
public bool IsImage { get; set; }
public bool IsDeleted { get; set; }
public int? PixelWidth { get; set; }
public AssetMetadata Metadata { get; set; }
public int? PixelHeight { get; set; }
public AssetType Type { get; set; }
public Guid AssetId
{

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs

@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
case RenameAssetFolder renameAssetFolder:
return UpdateReturn(renameAssetFolder, c =>
{
GuardAssetFolder.CanRename(c, Snapshot.FolderName);
GuardAssetFolder.CanRename(c);
Rename(c);

10
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
case AnnotateAsset annotateAsset:
return UpdateReturnAsync(annotateAsset, async c =>
{
GuardAsset.CanAnnotate(c, Snapshot.FileName!, Snapshot.Slug);
GuardAsset.CanAnnotate(c);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
@ -126,13 +126,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var @event = SimpleMapper.Map(command, new AssetCreated
{
IsImage = command.ImageInfo != null,
FileName = command.File.FileName,
FileSize = command.File.FileSize,
FileVersion = 0,
MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
Slug = command.File.FileName.ToAssetSlug()
});
@ -147,10 +144,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
FileVersion = Snapshot.FileVersion + 1,
FileSize = command.File.FileSize,
MimeType = command.File.MimeType,
PixelWidth = command.ImageInfo?.PixelWidth,
PixelHeight = command.ImageInfo?.PixelHeight,
IsImage = command.ImageInfo != null
MimeType = command.File.MimeType
});
RaiseEvent(@event);

3
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AnnotateAsset.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
@ -16,5 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public string? Slug { get; set; }
public HashSet<string> Tags { get; set; }
public AssetMetadata Metadata { get; set; }
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public Guid ParentId { get; set; }
public HashSet<string> Tags { get; set; }
public HashSet<string> Tags { get; } = new HashSet<string>();
public CreateAsset()
{

5
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
@ -13,7 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public AssetFile File { get; set; }
public ImageInfo? ImageInfo { get; set; }
public AssetMetadata Metadata { get; } = new AssetMetadata();
public AssetType Type { get; set; }
public string FileHash { get; set; }
}

222
backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs

@ -0,0 +1,222 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Tasks;
using TagLib;
using TagLib.Image;
using static TagLib.File;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class FileTagAssetMetadataSource : IAssetMetadataSource
{
private sealed class FileAbstraction : IFileAbstraction
{
private readonly AssetFile file;
public string Name
{
get { return file.FileName; }
}
public Stream ReadStream
{
get { return file.OpenRead(); }
}
public Stream WriteStream
{
get { throw new NotSupportedException(); }
}
public FileAbstraction(AssetFile file)
{
this.file = file;
}
public void CloseStream(Stream stream)
{
stream.Close();
}
}
public Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags)
{
Enhance(command, tags);
return TaskHelper.Done;
}
private void Enhance(UploadAssetCommand command, HashSet<string>? tags)
{
try
{
using (var file = Create(new FileAbstraction(command.File), ReadStyle.Average))
{
if (file.Properties == null)
{
return;
}
var type = file.Properties.MediaTypes;
if (type == MediaTypes.Audio)
{
command.Type = AssetType.Audio;
}
else if (type == MediaTypes.Photo)
{
command.Type = AssetType.Image;
}
else if (type.HasFlag(MediaTypes.Video))
{
command.Type = AssetType.Video;
}
var pw = file.Properties.PhotoWidth;
var ph = file.Properties.PhotoHeight;
if (pw > 0 && pw > 0)
{
command.Metadata.SetPixelWidth(pw);
command.Metadata.SetPixelHeight(ph);
if (tags != null)
{
tags.Add("image");
var wh = pw + ph;
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
}
}
}
void TryAddString(string name, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
{
command.Metadata.Add(name, JsonValue.Create(value));
}
}
void TryAddInt(string name, int? value)
{
if (value > 0)
{
command.Metadata.Add(name, JsonValue.Create(value));
}
}
void TryAddDouble(string name, double? value)
{
if (value > 0)
{
command.Metadata.Add(name, JsonValue.Create(value));
}
}
void TryAddTimeSpan(string name, TimeSpan value)
{
if (value != TimeSpan.Zero)
{
command.Metadata.Add(name, JsonValue.Create(value.ToString()));
}
}
if (file.Tag is ImageTag imageTag)
{
TryAddDouble("locationLatitude", imageTag.Latitude);
TryAddDouble("locationLongitude", imageTag.Longitude);
TryAddString("created", imageTag.DateTime?.ToIso8601());
}
TryAddTimeSpan("duration", file.Properties.Duration);
TryAddInt("audioBitrate", file.Properties.AudioBitrate);
TryAddInt("audioChannels", file.Properties.AudioChannels);
TryAddInt("audioSampleRate", file.Properties.AudioSampleRate);
TryAddInt("bitsPerSample", file.Properties.BitsPerSample);
TryAddInt("imageQuality", file.Properties.PhotoQuality);
TryAddInt("videoWidth", file.Properties.VideoWidth);
TryAddInt("videoHeight", file.Properties.VideoHeight);
TryAddString("description", file.Properties.Description);
}
}
catch
{
return;
}
}
public IEnumerable<string> Format(IAssetEntity asset)
{
var metadata = asset.Metadata;
switch (asset.Type)
{
case AssetType.Image:
{
if (metadata.TryGetNumber("pixelWidth", out var w) &&
metadata.TryGetNumber("pixelHeight", out var h))
{
yield return $"{w}x{h}px";
}
break;
}
case AssetType.Video:
{
if (metadata.TryGetNumber("videoWidth", out var w) &&
metadata.TryGetNumber("videoHeight", out var h))
{
yield return $"{w}x{h}pt";
}
if (metadata.TryGetString("duration", out var duration))
{
yield return duration;
}
break;
}
case AssetType.Audio:
{
if (metadata.TryGetString("duration", out var duration))
{
yield return duration;
}
break;
}
}
}
}
}

25
backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs

@ -6,22 +6,33 @@
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class FileTypeTagGenerator : ITagGenerator<CreateAsset>
public sealed class FileTypeTagGenerator : IAssetMetadataSource
{
public void GenerateTags(CreateAsset source, HashSet<string> tags)
public Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags)
{
var extension = source.File?.FileName?.FileType();
if (!string.IsNullOrWhiteSpace(extension))
if (tags != null)
{
tags.Add($"type/{extension.ToLowerInvariant()}");
var extension = command.File?.FileName?.FileType();
if (!string.IsNullOrWhiteSpace(extension))
{
tags.Add($"type/{extension.ToLowerInvariant()}");
}
}
return TaskHelper.Done;
}
public IEnumerable<string> Format(IAssetEntity asset)
{
yield break;
}
}
}

17
backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
public static class GuardAsset
{
public static void CanAnnotate(AnnotateAsset command, string oldFileName, string oldSlug)
public static void CanAnnotate(AnnotateAsset command)
{
Guard.NotNull(command);
@ -23,9 +23,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
if (string.IsNullOrWhiteSpace(command.FileName) &&
string.IsNullOrWhiteSpace(command.Slug) &&
command.Tags == null)
command.Tags == null &&
command.Metadata == null)
{
e("Either file name, slug or tags must be defined.", nameof(command.FileName), nameof(command.Slug), nameof(command.Tags));
e("Either file name, slug, tags or metadata must be defined.",
nameof(command.FileName),
nameof(command.Slug),
nameof(command.Tags),
nameof(command.Metadata));
}
});
}
@ -46,11 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
return Validate.It(() => "Cannot move asset.", async e =>
{
if (command.ParentId == oldParentId)
{
e("Asset is already part of this folder.", nameof(command.ParentId));
}
else
if (command.ParentId != oldParentId)
{
await CheckPathAsync(command.ParentId, assetQuery, e);
}

12
backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
});
}
public static void CanRename(RenameAssetFolder command, string olderFolderName)
public static void CanRename(RenameAssetFolder command)
{
Guard.NotNull(command);
@ -40,10 +40,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
e(Not.Defined("Folder name"), nameof(command.FolderName));
}
else if (string.Equals(command.FolderName, olderFolderName))
{
e(Not.New("Asset folder", "name"), nameof(command.FolderName));
}
});
}
@ -53,11 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
return Validate.It(() => "Cannot move asset.", async e =>
{
if (command.ParentId == oldParentId)
{
e("Asset folder is already part of this folder.", nameof(command.ParentId));
}
else
if (command.ParentId != oldParentId)
{
await CheckPathAsync(command.ParentId, assetQuery, id, e);
}

20
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs

@ -0,0 +1,20 @@
// ==========================================================================
// 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 Squidex.Domain.Apps.Entities.Assets.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetMetadataSource
{
Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags);
IEnumerable<string> Format(IAssetEntity asset);
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs

@ -12,5 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public interface IEnrichedAssetEntity : IAssetEntity
{
HashSet<string> TagNames { get; }
string MetadataText { get; }
}
}

69
backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class ImageMetadataSource : IAssetMetadataSource
{
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public ImageMetadataSource(IAssetThumbnailGenerator assetThumbnailGenerator)
{
Guard.NotNull(assetThumbnailGenerator);
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
public async Task EnhanceAsync(UploadAssetCommand command, HashSet<string>? tags)
{
var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
if (imageInfo != null)
{
command.Type = AssetType.Image;
command.Metadata.SetPixelWidth(imageInfo.PixelWidth);
command.Metadata.SetPixelHeight(imageInfo.PixelHeight);
if (tags != null)
{
tags.Add("image");
var wh = imageInfo.PixelWidth + imageInfo.PixelHeight;
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
}
}
}
}
public IEnumerable<string> Format(IAssetEntity asset)
{
if (asset.Type == AssetType.Image)
{
yield return $"{asset.Metadata.GetPixelWidth()}x{asset.Metadata.GetPixelHeight()}px";
}
}
}
}

39
backend/src/Squidex.Domain.Apps.Entities/Assets/ImageTagGenerator.cs

@ -1,39 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Tags;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class ImageTagGenerator : ITagGenerator<CreateAsset>
{
public void GenerateTags(CreateAsset source, HashSet<string> tags)
{
if (source.ImageInfo != null)
{
tags.Add("image");
var wh = source.ImageInfo.PixelWidth + source.ImageInfo.PixelHeight;
if (wh > 2000)
{
tags.Add("image/large");
}
else if (wh > 1000)
{
tags.Add("image/medium");
}
else
{
tags.Add("image/small");
}
}
}
}
}

43
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs

@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure;
@ -18,12 +19,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
public sealed class AssetEnricher : IAssetEnricher
{
private readonly ITagService tagService;
private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources;
public AssetEnricher(ITagService tagService)
public AssetEnricher(ITagService tagService, IEnumerable<IAssetMetadataSource> assetMetadataSources)
{
Guard.NotNull(tagService);
Guard.NotNull(assetMetadataSources);
this.tagService = tagService;
this.assetMetadataSources = assetMetadataSources;
}
public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context)
@ -48,12 +52,49 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
if (ShouldEnrich(context))
{
await EnrichTagsAsync(results);
EnrichWithMetadataText(results);
}
return results;
}
}
private void EnrichWithMetadataText(List<AssetEntity> results)
{
var sb = new StringBuilder();
void Append(string? text)
{
if (!string.IsNullOrWhiteSpace(text))
{
if (sb.Length > 0)
{
sb.Append(", ");
}
sb.Append(text);
}
}
foreach (var asset in results)
{
sb.Clear();
foreach (var source in assetMetadataSources)
{
foreach (var metadata in source.Format(asset))
{
Append(metadata);
}
}
Append(asset.FileSize.ToReadableSize());
asset.MetadataText = sb.ToString();
}
}
private async Task EnrichTagsAsync(List<AssetEntity> assets)
{
foreach (var group in assets.GroupBy(x => x.AppId.Id))

34
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs

@ -116,19 +116,18 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
AddProperty(nameof(IAssetEntity.Id), JsonObjectType.String, JsonFormatStrings.Guid);
AddProperty(nameof(IAssetEntity.Created), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.CreatedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileHash), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileName), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.FileSize), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.FileVersion), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.IsImage), JsonObjectType.Boolean);
AddProperty(nameof(IAssetEntity.LastModified), JsonObjectType.String, JsonFormatStrings.DateTime);
AddProperty(nameof(IAssetEntity.LastModifiedBy), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Metadata), JsonObjectType.None);
AddProperty(nameof(IAssetEntity.MimeType), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.PixelHeight), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.PixelWidth), JsonObjectType.Integer);
AddProperty(nameof(IAssetEntity.Slug), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Tags), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Type), JsonObjectType.String);
AddProperty(nameof(IAssetEntity.Version), JsonObjectType.Integer);
return schema;
}
@ -142,22 +141,29 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
entityType.AddStructuralProperty(name.ToCamelCase(), type);
}
void AddPropertyReference(string name, IEdmTypeReference reference)
{
entityType.AddStructuralProperty(name.ToCamelCase(), reference);
}
var jsonType = new EdmComplexType("Squidex", "Json", null, false, true);
AddPropertyReference(nameof(IAssetEntity.Metadata), new EdmComplexTypeReference(jsonType, false));
AddProperty(nameof(IAssetEntity.Id), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Created), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.CreatedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileHash), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileName), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.FileSize), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.FileVersion), EdmPrimitiveTypeKind.Int64);
AddProperty(nameof(IAssetEntity.IsImage), EdmPrimitiveTypeKind.Boolean);
AddProperty(nameof(IAssetEntity.LastModified), EdmPrimitiveTypeKind.DateTimeOffset);
AddProperty(nameof(IAssetEntity.LastModifiedBy), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.MimeType), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.PixelHeight), EdmPrimitiveTypeKind.Int32);
AddProperty(nameof(IAssetEntity.PixelWidth), EdmPrimitiveTypeKind.Int32);
AddProperty(nameof(IAssetEntity.Slug), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Tags), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Type), EdmPrimitiveTypeKind.String);
AddProperty(nameof(IAssetEntity.Version), EdmPrimitiveTypeKind.Int64);
var container = new EdmEntityContainer("Squidex", "Container");

23
backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets.State
{
public class AssetFolderState : DomainObjectState<AssetFolderState>, IAssetFolderEntity
public sealed class AssetFolderState : DomainObjectState<AssetFolderState>, IAssetFolderEntity
{
[DataMember]
public NamedId<Guid> AppId { get; set; }
@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[DataMember]
public Guid ParentId { get; set; }
public void ApplyEvent(IEvent @event)
public override bool ApplyEvent(IEvent @event)
{
switch (@event)
{
@ -38,35 +38,32 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
{
SimpleMapper.Map(e, this);
break;
return true;
}
case AssetFolderRenamed e:
case AssetFolderRenamed e when e.FolderName != FolderName:
{
SimpleMapper.Map(e, this);
FolderName = e.FolderName;
break;
return true;
}
case AssetFolderMoved e:
case AssetFolderMoved e when e.ParentId != ParentId:
{
ParentId = e.ParentId;
break;
return true;
}
case AssetFolderDeleted _:
{
IsDeleted = true;
break;
return true;
}
}
}
public override AssetFolderState Apply(Envelope<IEvent> @event)
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
return false;
}
}
}

50
backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@ -47,26 +48,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
public long TotalSize { get; set; }
[DataMember]
public bool IsImage { get; set; }
public HashSet<string> Tags { get; set; }
[DataMember]
public int? PixelWidth { get; set; }
public AssetMetadata Metadata { get; set; }
[DataMember]
public int? PixelHeight { get; set; }
public AssetType Type { get; set; }
[DataMember]
public bool IsDeleted { get; set; }
[DataMember]
public HashSet<string> Tags { get; set; }
public Guid AssetId
{
get { return Id; }
}
public void ApplyEvent(IEvent @event)
public override bool ApplyEvent(IEvent @event)
{
switch (@event)
{
@ -87,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
TotalSize += e.FileSize;
break;
return true;
}
case AssetUpdated e:
@ -96,48 +94,60 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
TotalSize += e.FileSize;
break;
return true;
}
case AssetAnnotated e:
{
if (!string.IsNullOrWhiteSpace(e.FileName))
var hasChanged = false;
if (!string.IsNullOrWhiteSpace(e.FileName) && !string.Equals(e.FileName, FileName))
{
FileName = e.FileName;
hasChanged = true;
}
if (!string.IsNullOrWhiteSpace(e.Slug))
if (!string.IsNullOrWhiteSpace(e.Slug) && !string.Equals(e.Slug, Slug))
{
Slug = e.Slug;
hasChanged = true;
}
if (e.Tags != null)
if (e.Tags != null && !e.Tags.SetEquals(Tags))
{
Tags = e.Tags;
hasChanged = true;
}
break;
if (e.Metadata != null && !e.Metadata.EqualsDictionary(Metadata))
{
Metadata = e.Metadata;
hasChanged = true;
}
return hasChanged;
}
case AssetMoved e:
case AssetMoved e when e.ParentId != ParentId:
{
ParentId = e.ParentId;
break;
return true;
}
case AssetDeleted _:
{
IsDeleted = true;
break;
return true;
}
}
}
public override AssetState Apply(Envelope<IEvent> @event)
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
return false;
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName)
public (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName)
{
return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName));
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs

@ -33,6 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IObjectGraphType GetContentType(Guid schemaId);
(IGraphType? ResolveType, ValueResolver? Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName);
(IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName);
}
}

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

@ -7,12 +7,15 @@
using System;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static class AllTypes
{
public const string PathName = "path";
public static readonly Type None = typeof(NoopGraphType);
public static readonly Type NonNullTagsType = typeof(NonNullGraphType<ListGraphType<NonNullGraphType<StringGraphType>>>);
@ -31,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType Boolean = new BooleanGraphType();
public static readonly IGraphType AssetType = new EnumerationGraphType<AssetType>();
public static readonly IGraphType NonNullInt = new NonNullGraphType(Int);
public static readonly IGraphType NonNullGuid = new NonNullGraphType(Guid);
@ -43,6 +48,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean);
public static readonly IGraphType NonNullAssetType = new NonNullGraphType(AssetType);
public static readonly IGraphType NoopDate = new NoopGraphType(Date);
public static readonly IGraphType NoopJson = new NoopGraphType(Json);
@ -53,8 +60,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType NoopBoolean = new NoopGraphType(Boolean);
public static readonly IGraphType NoopTags = new NoopGraphType("Tags");
public static readonly IGraphType NoopTags = new NoopGraphType("TagsScalar");
public static readonly IGraphType NoopGeolocation = new NoopGraphType("GeolocationScalar");
public static readonly IGraphType NoopGeolocation = new NoopGraphType("Geolocation");
public static readonly QueryArguments PathArguments = new QueryArguments(new QueryArgument(None)
{
Name = PathName,
Description = $"The path to the json value",
DefaultValue = null,
ResolvedType = String
});
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = $"find{schemaType}Content",
Arguments = CreateContentFindTypes(schemaName),
Arguments = CreateContentFindArguments(schemaName),
ResolvedType = contentType,
Resolver = ResolveAsync((c, e) =>
{
@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
};
}
private static QueryArguments CreateContentFindTypes(string schemaName)
private static QueryArguments CreateContentFindArguments(string schemaName)
{
return new QueryArguments
{

53
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -8,6 +8,7 @@
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
@ -143,24 +144,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "isImage",
ResolvedType = AllTypes.NonNullBoolean,
Resolver = Resolve(x => x.IsImage),
Description = "Determines of the created file is an image."
Resolver = Resolve(x => x.Type == AssetType.Image),
Description = "Determines if the uploaded file is an image.",
DeprecationReason = "Use 'type' field instead."
});
AddField(new FieldType
{
Name = "pixelWidth",
ResolvedType = AllTypes.Int,
Resolver = Resolve(x => x.PixelWidth),
Description = "The width of the image in pixels if the asset is an image."
Resolver = Resolve(x => x.Metadata.GetPixelWidth()),
Description = "The width of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead."
});
AddField(new FieldType
{
Name = "pixelHeight",
ResolvedType = AllTypes.Int,
Resolver = Resolve(x => x.PixelHeight),
Description = "The height of the image in pixels if the asset is an image."
Resolver = Resolve(x => x.Metadata.GetPixelHeight()),
Description = "The height of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead."
});
AddField(new FieldType
{
Name = "type",
ResolvedType = AllTypes.NonNullAssetType,
Resolver = Resolve(x => x.Type),
Description = "The type of the image."
});
AddField(new FieldType
{
Name = "metadataText",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.MetadataText),
Description = "The text representation of the metadata."
});
AddField(new FieldType
@ -172,6 +192,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Type = AllTypes.NonNullTagsType
});
AddField(new FieldType
{
Name = "metadata",
Arguments = AllTypes.PathArguments,
ResolvedType = AllTypes.NoopJson,
Resolver = ResolveMetadata(),
Description = "The asset metadata.",
});
if (model.CanGenerateAssetSourceUrl)
{
AddField(new FieldType
@ -186,6 +215,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "An asset";
}
private static IFieldResolver ResolveMetadata()
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c =>
{
var path = c.Arguments.GetOrDefault(AllTypes.PathName);
c.Source.Metadata.TryGetByPath(path as string, out var result);
return result;
});
}
private static IFieldResolver Resolve(Func<IEnrichedAssetEntity, object?> action)
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source));

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs

@ -20,13 +20,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var (field, fieldName, _) in schema.SchemaDef.Fields.SafeFields())
{
var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName);
var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, fieldName);
if (valueResolver != null)
{
AddField(new FieldType
{
Name = fieldName,
Arguments = args,
Resolver = PartitionResolver(valueResolver, field.Name),
ResolvedType = resolvedType,
Description = field.RawProperties.Hints

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields())
{
var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName);
var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, fieldName);
if (valueResolver != null)
{
@ -44,6 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
fieldGraphType.AddField(new FieldType
{
Name = key.EscapePartition(),
Arguments = args,
Resolver = PartitionResolver(valueResolver, key),
ResolvedType = resolvedType,
Description = field.RawProperties.Hints

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs

@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields())
{
var (resolveType, valueResolver) = model.GetGraphType(schema, nestedField, nestedName);
var (resolveType, valueResolver, args) = model.GetGraphType(schema, nestedField, nestedName);
if (resolveType != null && valueResolver != null)
{
@ -35,6 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = nestedName,
Arguments = args,
Resolver = resolver,
ResolvedType = resolveType,
Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field."

61
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context);
public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType? ResolveType, ValueResolver? Resolver)>
public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType?, ValueResolver?, QueryArguments?)>
{
private static readonly ValueResolver NoopResolver = (value, c) => value;
private readonly Dictionary<Guid, ContentGraphType> schemaTypes;
@ -40,74 +40,83 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
this.fieldName = fieldName;
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IArrayField field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IArrayField field)
{
return ResolveNested(field);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<AssetsFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<AssetsFieldProperties> field)
{
return ResolveAssets();
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<BooleanFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<BooleanFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopBoolean);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<DateTimeFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<DateTimeFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopDate);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<GeolocationFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<GeolocationFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopGeolocation);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<JsonFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopJson);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<NumberFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<NumberFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopFloat);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<ReferencesFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<ReferencesFieldProperties> field)
{
return ResolveReferences(field);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<StringFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<StringFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopString);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<TagsFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<TagsFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopTags);
}
public (IGraphType? ResolveType, ValueResolver? Resolver) Visit(IField<UIFieldProperties> field)
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<UIFieldProperties> field)
{
return (null, null);
return (null, null, null);
}
private static (IGraphType? ResolveType, ValueResolver? Resolver) ResolveDefault(IGraphType type)
private static (IGraphType?, ValueResolver?, QueryArguments?) ResolveDefault(IGraphType type)
{
return (type, NoopResolver);
return (type, NoopResolver, null);
}
private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveNested(IArrayField field)
private (IGraphType?, ValueResolver?, QueryArguments?) ResolveNested(IArrayField field)
{
var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName)));
return (schemaFieldType, NoopResolver);
return (schemaFieldType, NoopResolver, null);
}
public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField<JsonFieldProperties> field)
{
var resolver = new ValueResolver((value, c) =>
{
var path = c.Arguments.GetOrDefault(AllTypes.PathName);
value.TryGetByPath(path as string, out var result);
return result!;
});
return (AllTypes.NoopJson, resolver, AllTypes.PathArguments);
}
private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveAssets()
private (IGraphType?, ValueResolver?, QueryArguments?) ResolveAssets()
{
var resolver = new ValueResolver((value, c) =>
{
@ -116,10 +125,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return context.GetReferencedAssetsAsync(value);
});
return (assetListType, resolver);
return (assetListType, resolver, null);
}
private (IGraphType? ResolveType, ValueResolver? Resolver) ResolveReferences(IField<ReferencesFieldProperties> field)
private (IGraphType?, ValueResolver?, QueryArguments?) ResolveReferences(IField<ReferencesFieldProperties> field)
{
IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId());
@ -129,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
if (!union.PossibleTypes.Any())
{
return (null, null);
return (null, null, null);
}
contentType = union;
@ -144,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType));
return (schemaFieldType, resolver);
return (schemaFieldType, resolver, null);
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{
public JsonGraphType()
{
Name = "Json";
Name = "JsonScalar";
Description = "Unstructured Json object";
}

3
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
@ -249,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
field.GetReferencedIds(partitionValue, Ids.ContentOnly)
.Select(x => assets[x])
.SelectMany(x => x)
.FirstOrDefault(x => x.IsImage);
.FirstOrDefault(x => x.Type == AssetType.Image);
if (referencedImage != null)
{

9
backend/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs

@ -17,7 +17,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents.State
{
public class ContentState : DomainObjectState<ContentState>, IContentEntity
public sealed class ContentState : DomainObjectState<ContentState>, IContentEntity
{
[DataMember]
public NamedId<Guid> AppId { get; set; }
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
[DataMember]
public Status Status { get; set; }
public void ApplyEvent(IEvent @event)
public override bool ApplyEvent(IEvent @event)
{
switch (@event)
{
@ -121,11 +121,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.State
break;
}
}
}
public override ContentState Apply(Envelope<IEvent> @event)
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
return true;
}
private void UpdateData(NamedContentData? data, NamedContentData? dataDraft, bool isPending)

2
backend/src/Squidex.Domain.Apps.Entities/EntityExtensions.cs → backend/src/Squidex.Domain.Apps.Entities/DomainEntityExtensions.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities
{
public static class EntityExtensions
public static class DomainEntityExtensions
{
public static NamedId<Guid> NamedId(this IAppEntity entity)
{

43
backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs

@ -8,22 +8,20 @@
using System;
using System.Runtime.Serialization;
using NodaTime;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities
{
public abstract class DomainObjectState<T> : Cloneable<T>,
public abstract class DomainObjectState<T> :
IDomainState<T>,
IEntity,
IEntityWithCreatedBy,
IEntityWithLastModifiedBy,
IEntityWithVersion,
IUpdateableEntity,
IUpdateableEntityWithCreatedBy,
IUpdateableEntityWithLastModifiedBy
where T : Cloneable
IEntityWithVersion
where T : class
{
[DataMember]
public Guid Id { get; set; }
@ -43,11 +41,38 @@ namespace Squidex.Domain.Apps.Entities
[DataMember]
public long Version { get; set; } = EtagVersion.Empty;
public abstract T Apply(Envelope<IEvent> @event);
public abstract bool ApplyEvent(IEvent @event);
public T Clone()
public T Apply(Envelope<IEvent> @event)
{
return Clone(x => { });
var payload = (SquidexEvent)@event.Payload;
var clone = (DomainObjectState<T>)MemberwiseClone();
if (!clone.ApplyEvent(@event.Payload))
{
return (this as T)!;
}
var headers = @event.Headers;
if (clone.Id == default)
{
clone.Id = headers.AggregateId();
}
if (clone.CreatedBy == null)
{
clone.Created = headers.Timestamp();
clone.CreatedBy = payload.Actor;
}
clone.LastModified = headers.Timestamp();
clone.LastModifiedBy = payload.Actor;
clone.Version = headers.EventStreamNumber();
return (clone as T)!;
}
}
}

82
backend/src/Squidex.Domain.Apps.Entities/EntityMapper.cs

@ -1,82 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities
{
public static class EntityMapper
{
public static T Update<T>(this T entity, Envelope<IEvent> envelope, Action<SquidexEvent, T>? updater = null) where T : IEntity
{
var @event = (SquidexEvent)envelope.Payload;
var headers = envelope.Headers;
SetId(entity, headers);
SetCreated(entity, headers);
SetCreatedBy(entity, @event);
SetLastModified(entity, headers);
SetLastModifiedBy(entity, @event);
SetVersion(entity, headers);
updater?.Invoke(@event, entity);
return entity;
}
private static void SetId(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty)
{
updateable.Id = headers.AggregateId();
}
}
private static void SetVersion(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntityWithVersion updateable)
{
updateable.Version = headers.EventStreamNumber();
}
}
private static void SetCreated(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntity updateable && updateable.Created == default)
{
updateable.Created = headers.Timestamp();
}
}
private static void SetCreatedBy(IEntity entity, SquidexEvent @event)
{
if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null)
{
withCreatedBy.CreatedBy = @event.Actor;
}
}
private static void SetLastModified(IEntity entity, EnvelopeHeaders headers)
{
if (entity is IUpdateableEntity updateable)
{
updateable.LastModified = headers.Timestamp();
}
}
private static void SetLastModifiedBy(IEntity entity, SquidexEvent @event)
{
if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy)
{
withModifiedBy.LastModifiedBy = @event.Actor;
}
}
}
}

21
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs

@ -1,21 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Domain.Apps.Entities
{
public interface IUpdateableEntity
{
Guid Id { get; set; }
Instant Created { get; set; }
Instant LastModified { get; set; }
}
}

16
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithCreatedBy.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities
{
public interface IUpdateableEntityWithCreatedBy
{
RefToken CreatedBy { get; set; }
}
}

16
backend/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithLastModifiedBy.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities
{
public interface IUpdateableEntityWithLastModifiedBy
{
RefToken LastModifiedBy { get; set; }
}
}

17
backend/src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs

@ -7,7 +7,6 @@
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
@ -46,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
});
}
public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider, Rule rule)
public static Task CanUpdate(UpdateRule command, Guid appId, IAppProvider appProvider)
{
Guard.NotNull(command);
@ -73,24 +72,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
});
}
public static void CanEnable(EnableRule command, Rule rule)
public static void CanEnable(EnableRule command)
{
Guard.NotNull(command);
if (rule.IsEnabled)
{
throw new DomainException("Rule is already enabled.");
}
}
public static void CanDisable(DisableRule command, Rule rule)
public static void CanDisable(DisableRule command)
{
Guard.NotNull(command);
if (!rule.IsEnabled)
{
throw new DomainException("Rule is already disabled.");
}
}
public static void CanDelete(DeleteRule command)

6
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
case UpdateRule updateRule:
return UpdateReturnAsync(updateRule, async c =>
{
await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider, Snapshot.RuleDef);
await GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider);
Update(c);
@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
case EnableRule enableRule:
return UpdateReturn(enableRule, c =>
{
GuardRule.CanEnable(c, Snapshot.RuleDef);
GuardRule.CanEnable(c);
Enable(c);
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
case DisableRule disableRule:
return UpdateReturn(disableRule, c =>
{
GuardRule.CanDisable(c, Snapshot.RuleDef);
GuardRule.CanDisable(c);
Disable(c);

15
backend/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs

@ -18,7 +18,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules.State
{
[CollectionName("Rules")]
public class RuleState : DomainObjectState<RuleState>, IRuleEntity
public sealed class RuleState : DomainObjectState<RuleState>, IRuleEntity
{
[DataMember]
public NamedId<Guid> AppId { get; set; }
@ -29,8 +29,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.State
[DataMember]
public bool IsDeleted { get; set; }
public void ApplyEvent(IEvent @event)
public override bool ApplyEvent(IEvent @event)
{
var previousRule = RuleDef;
switch (@event)
{
case RuleCreated e:
@ -40,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.State
AppId = e.AppId;
break;
return true;
}
case RuleUpdated e:
@ -81,14 +83,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.State
{
IsDeleted = true;
break;
return true;
}
}
}
public override RuleState Apply(Envelope<IEvent> @event)
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
return !ReferenceEquals(previousRule, RuleDef);
}
}
}

34
backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
});
}
public static void CanReorder(Schema schema, ReorderFields command)
public static void CanReorder(ReorderFields command, Schema schema)
{
Guard.NotNull(command);
@ -88,53 +88,43 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
});
}
public static void CanPublish(Schema schema, PublishSchema command)
public static void CanConfigureUIFields(ConfigureUIFields command, Schema schema)
{
Guard.NotNull(command);
if (schema.IsPublished)
Validate.It(() => "Cannot configure UI fields.", e =>
{
throw new DomainException("Schema is already published.");
}
ValidateFieldNames(schema, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField);
ValidateFieldNames(schema, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed);
});
}
public static void CanUnpublish(Schema schema, UnpublishSchema command)
public static void CanPublish(PublishSchema command)
{
Guard.NotNull(command);
if (!schema.IsPublished)
{
throw new DomainException("Schema is not published.");
}
}
public static void CanConfigureUIFields(Schema schema, ConfigureUIFields command)
public static void CanUnpublish(UnpublishSchema command)
{
Guard.NotNull(command);
Validate.It(() => "Cannot configure UI fields.", e =>
{
ValidateFieldNames(schema, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField);
ValidateFieldNames(schema, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed);
});
}
public static void CanUpdate(Schema schema, UpdateSchema command)
public static void CanUpdate(UpdateSchema command)
{
Guard.NotNull(command);
}
public static void CanConfigureScripts(Schema schema, ConfigureScripts command)
public static void CanConfigureScripts(ConfigureScripts command)
{
Guard.NotNull(command);
}
public static void CanChangeCategory(Schema schema, ChangeCategory command)
public static void CanChangeCategory(ChangeCategory command)
{
Guard.NotNull(command);
}
public static void CanDelete(Schema schema, DeleteSchema command)
public static void CanDelete(DeleteSchema command)
{
Guard.NotNull(command);
}

52
backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
public static class GuardSchemaField
{
public static void CanAdd(Schema schema, AddField command)
public static void CanAdd(AddField command, Schema schema)
{
Guard.NotNull(command);
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
});
}
public static void CanUpdate(Schema schema, UpdateField command)
public static void CanUpdate(UpdateField command, Schema schema)
{
Guard.NotNull(command);
@ -82,86 +82,66 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
});
}
public static void CanHide(Schema schema, HideField command)
public static void CanHide(HideField command, Schema schema)
{
Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsHidden)
{
throw new DomainException("Schema field is already hidden.");
}
if (!field.IsForApi())
if (!field.IsForApi(true))
{
throw new DomainException("UI field cannot be hidden.");
}
}
public static void CanShow(Schema schema, ShowField command)
public static void CanDisable(DisableField command, Schema schema)
{
Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (!field.IsHidden)
if (!field.IsForApi(true))
{
throw new DomainException("Schema field is already visible.");
throw new DomainException("UI field cannot be diabled.");
}
}
public static void CanDisable(Schema schema, DisableField command)
public static void CanShow(ShowField command, Schema schema)
{
Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsDisabled)
{
throw new DomainException("Schema field is already disabled.");
}
if (!field.IsForApi(true))
{
throw new DomainException("UI field cannot be disabled.");
throw new DomainException("UI field cannot be shown.");
}
}
public static void CanDelete(Schema schema, DeleteField command)
public static void CanEnable(EnableField command, Schema schema)
{
Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsLocked)
if (!field.IsForApi(true))
{
throw new DomainException("Schema field is locked.");
throw new DomainException("UI field cannot be enabled.");
}
}
public static void CanEnable(Schema schema, EnableField command)
public static void CanDelete(DeleteField command, Schema schema)
{
Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (!field.IsDisabled)
{
throw new DomainException("Schema field is already enabled.");
}
GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
}
public static void CanLock(Schema schema, LockField command)
public static void CanLock(LockField command, Schema schema)
{
Guard.NotNull(command);
var field = GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, false);
if (field.IsLocked)
{
throw new DomainException("Schema field is already locked.");
}
GuardHelper.GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId, true);
}
}
}

42
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -17,7 +17,6 @@ using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
@ -27,14 +26,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{
public sealed class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain
{
private readonly IJsonSerializer serializer;
public SchemaGrain(IStore<Guid> store, ISemanticLog log, IJsonSerializer serializer)
public SchemaGrain(IStore<Guid> store, ISemanticLog log)
: base(store, log)
{
Guard.NotNull(serializer);
this.serializer = serializer;
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
@ -46,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case AddField addField:
return UpdateReturn(addField, c =>
{
GuardSchemaField.CanAdd(Snapshot.SchemaDef, c);
GuardSchemaField.CanAdd(c, Snapshot.SchemaDef);
Add(c);
@ -87,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case DeleteField deleteField:
return UpdateReturn(deleteField, c =>
{
GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField);
GuardSchemaField.CanDelete(deleteField, Snapshot.SchemaDef);
DeleteField(c);
@ -97,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case LockField lockField:
return UpdateReturn(lockField, c =>
{
GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField);
GuardSchemaField.CanLock(lockField, Snapshot.SchemaDef);
LockField(c);
@ -107,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case HideField hideField:
return UpdateReturn(hideField, c =>
{
GuardSchemaField.CanHide(Snapshot.SchemaDef, c);
GuardSchemaField.CanHide(c, Snapshot.SchemaDef);
HideField(c);
@ -117,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ShowField showField:
return UpdateReturn(showField, c =>
{
GuardSchemaField.CanShow(Snapshot.SchemaDef, c);
GuardSchemaField.CanShow(c, Snapshot.SchemaDef);
ShowField(c);
@ -127,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case DisableField disableField:
return UpdateReturn(disableField, c =>
{
GuardSchemaField.CanDisable(Snapshot.SchemaDef, c);
GuardSchemaField.CanDisable(c, Snapshot.SchemaDef);
DisableField(c);
@ -137,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case EnableField enableField:
return UpdateReturn(enableField, c =>
{
GuardSchemaField.CanEnable(Snapshot.SchemaDef, c);
GuardSchemaField.CanEnable(c, Snapshot.SchemaDef);
EnableField(c);
@ -147,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case UpdateField updateField:
return UpdateReturn(updateField, c =>
{
GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c);
GuardSchemaField.CanUpdate(c, Snapshot.SchemaDef);
UpdateField(c);
@ -157,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ReorderFields reorderFields:
return UpdateReturn(reorderFields, c =>
{
GuardSchema.CanReorder(Snapshot.SchemaDef, c);
GuardSchema.CanReorder(c, Snapshot.SchemaDef);
Reorder(c);
@ -167,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case UpdateSchema updateSchema:
return UpdateReturn(updateSchema, c =>
{
GuardSchema.CanUpdate(Snapshot.SchemaDef, c);
GuardSchema.CanUpdate(c);
Update(c);
@ -177,7 +171,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case PublishSchema publishSchema:
return UpdateReturn(publishSchema, c =>
{
GuardSchema.CanPublish(Snapshot.SchemaDef, c);
GuardSchema.CanPublish(c);
Publish(c);
@ -187,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case UnpublishSchema unpublishSchema:
return UpdateReturn(unpublishSchema, c =>
{
GuardSchema.CanUnpublish(Snapshot.SchemaDef, c);
GuardSchema.CanUnpublish(c);
Unpublish(c);
@ -197,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ConfigureScripts configureScripts:
return UpdateReturn(configureScripts, c =>
{
GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c);
GuardSchema.CanConfigureScripts(c);
ConfigureScripts(c);
@ -207,7 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ChangeCategory changeCategory:
return UpdateReturn(changeCategory, c =>
{
GuardSchema.CanChangeCategory(Snapshot.SchemaDef, c);
GuardSchema.CanChangeCategory(c);
ChangeCategory(c);
@ -227,7 +221,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case ConfigureUIFields configureUIFields:
return UpdateReturn(configureUIFields, c =>
{
GuardSchema.CanConfigureUIFields(Snapshot.SchemaDef, c);
GuardSchema.CanConfigureUIFields(c, Snapshot.SchemaDef);
ConfigureUIFields(c);
@ -237,7 +231,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
case DeleteSchema deleteSchema:
return Update(deleteSchema, c =>
{
GuardSchema.CanDelete(Snapshot.SchemaDef, c);
GuardSchema.CanDelete(c);
Delete(c);
});
@ -258,7 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
var schemaSource = Snapshot.SchemaDef;
var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton);
var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options);
var events = schemaSource.Synchronize(schemaTarget, () => Snapshot.SchemaFieldsTotal + 1, options);
foreach (var @event in events)
{

15
backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs

@ -19,7 +19,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas.State
{
[CollectionName("Schemas")]
public class SchemaState : DomainObjectState<SchemaState>, ISchemaEntity
public sealed class SchemaState : DomainObjectState<SchemaState>, ISchemaEntity
{
[DataMember]
public NamedId<Guid> AppId { get; set; }
@ -33,8 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
[DataMember]
public Schema SchemaDef { get; set; }
public void ApplyEvent(IEvent @event)
public override bool ApplyEvent(IEvent @event)
{
var previousSchema = SchemaDef;
switch (@event)
{
case SchemaCreated e:
@ -44,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
AppId = e.AppId;
break;
return true;
}
case FieldAdded e:
@ -187,14 +189,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
{
IsDeleted = true;
break;
return true;
}
}
}
public override SchemaState Apply(Envelope<IEvent> @event)
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
return !ReferenceEquals(previousSchema, SchemaDef);
}
}
}

1
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -31,6 +31,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="taglib-sharp-netstandard2.0" Version="2.1.0" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

3
backend/src/Squidex.Domain.Apps.Events/Assets/AssetAnnotated.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
@ -17,6 +18,8 @@ namespace Squidex.Domain.Apps.Events.Assets
public string Slug { get; set; }
public AssetMetadata? Metadata { get; set; }
public HashSet<string>? Tags { get; set; }
}
}

9
backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs

@ -7,11 +7,12 @@
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
{
[EventType(nameof(AssetCreated))]
[EventType(nameof(AssetCreated), 2)]
public sealed class AssetCreated : AssetEvent
{
public Guid ParentId { get; set; }
@ -28,11 +29,9 @@ namespace Squidex.Domain.Apps.Events.Assets
public long FileSize { get; set; }
public bool IsImage { get; set; }
public AssetType Type { get; set; }
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; }
public AssetMetadata Metadata { get; set; }
public HashSet<string>? Tags { get; set; }
}

11
backend/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs

@ -5,11 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Reflection;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
{
[TypeName("AssetUpdated")]
[EventType(nameof(AssetUpdated), 2)]
public sealed class AssetUpdated : AssetEvent
{
public string MimeType { get; set; }
@ -20,10 +21,8 @@ namespace Squidex.Domain.Apps.Events.Assets
public long FileVersion { get; set; }
public bool IsImage { get; set; }
public AssetType Type { get; set; }
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; }
public AssetMetadata Metadata { get; set; }
}
}

12
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System;
using System.Globalization;
using MongoDB.Bson.IO;
using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter;
@ -142,19 +141,12 @@ namespace Squidex.Infrastructure.MongoDb
public override void WriteValue(DateTime value)
{
bsonWriter.WriteString(value.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture));
bsonWriter.WriteString(value.ToIso8601());
}
public override void WriteValue(DateTimeOffset value)
{
if (value.Offset == TimeSpan.Zero)
{
bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture));
}
else
{
bsonWriter.WriteString(value.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture));
}
bsonWriter.WriteString(value.UtcDateTime.ToIso8601());
}
public override void WriteValue(byte[]? value)

1
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/SortBuilder.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using MongoDB.Driver;
using Squidex.Infrastructure.Queries;

17
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -18,6 +18,11 @@ namespace Squidex.Infrastructure
return source.Intersect(other).Count() == other.Count;
}
public static bool SetEquals<T>(this ICollection<T> source, ICollection<T> other, IEqualityComparer<T> comparer)
{
return source.Intersect(other, comparer).Count() == other.Count;
}
public static IResultList<T> SortSet<T, TKey>(this IResultList<T> input, Func<T, TKey> idProvider, IReadOnlyList<TKey> ids) where T : class
{
return ResultList.Create(input.Total, SortList(input, idProvider, ids));
@ -162,9 +167,19 @@ namespace Squidex.Infrastructure
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue> other, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer) where TKey : notnull
{
if (other == null)
{
return false;
}
if (dictionary.Count != other.Count)
{
return false;
}
var comparer = new KeyValuePairComparer<TKey, TValue>(keyComparer, valueComparer);
return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any();
return !dictionary.Except(other, comparer).Any();
}
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull

136
backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs

@ -11,12 +11,14 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
#pragma warning disable IDE0044 // Add readonly modifier
namespace Squidex.Infrastructure.Collections
{
public class ArrayDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> where TKey : notnull
{
private readonly IEqualityComparer<TKey> keyComparer;
private readonly KeyValuePair<TKey, TValue>[] items;
private KeyValuePair<TKey, TValue>[] items;
public TValue this[TKey key]
{
@ -66,85 +68,131 @@ namespace Squidex.Infrastructure.Collections
this.keyComparer = keyComparer;
}
public KeyValuePair<TKey, TValue>[] With(TKey key, TValue value)
public bool IsUnchanged(KeyValuePair<TKey, TValue>[] values)
{
var result = new List<KeyValuePair<TKey, TValue>>(Math.Max(items.Length, 1));
return ReferenceEquals(values, items);
}
var wasReplaced = false;
public ArrayDictionary<TKey, TValue> With(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null)
{
return With<ArrayDictionary<TKey, TValue>>(key, value, valueComparer);
}
for (var i = 0; i < items.Length; i++)
public TArray With<TArray>(TKey key, TValue value, IEqualityComparer<TValue>? valueComparer = null) where TArray : ArrayDictionary<TKey, TValue>
{
var index = IndexOf(key);
if (index < 0)
{
var item = items[i];
var result = new KeyValuePair<TKey, TValue>[items.Length + 1];
if (wasReplaced || !keyComparer.Equals(item.Key, key))
{
result.Add(item);
}
else
{
result.Add(new KeyValuePair<TKey, TValue>(key, value));
wasReplaced = true;
}
Array.Copy(items, 0, result, 0, items.Length);
result[^1] = new KeyValuePair<TKey, TValue>(key, value);
return Create<TArray>(result);
}
if (!wasReplaced)
var existing = items[index].Value;
if (valueComparer == null || !valueComparer.Equals(value, existing))
{
result.Add(new KeyValuePair<TKey, TValue>(key, value));
var result = new KeyValuePair<TKey, TValue>[items.Length];
Array.Copy(items, 0, result, 0, items.Length);
result[index] = new KeyValuePair<TKey, TValue>(key, value);
return Create<TArray>(result);
}
return result.ToArray();
return Self<TArray>();
}
public KeyValuePair<TKey, TValue>[] Without(TKey key)
public ArrayDictionary<TKey, TValue> Without(TKey key)
{
var result = new List<KeyValuePair<TKey, TValue>>(Math.Max(items.Length, 1));
return Without<ArrayDictionary<TKey, TValue>>(key);
}
var wasRemoved = false;
public TArray Without<TArray>(TKey key) where TArray : ArrayDictionary<TKey, TValue>
{
var index = IndexOf(key);
for (var i = 0; i < items.Length; i++)
if (index < 0)
{
return Self<TArray>();
}
var result = Array.Empty<KeyValuePair<TKey, TValue>>();
if (items.Length > 1)
{
var item = items[i];
result = new KeyValuePair<TKey, TValue>[items.Length - 1];
if (wasRemoved || !keyComparer.Equals(item.Key, key))
{
result.Add(item);
}
else
{
wasRemoved = true;
}
var afterIndex = items.Length - index - 1;
Array.Copy(items, 0, result, 0, index);
Array.Copy(items, index, result, index, afterIndex);
}
return result.ToArray();
return Create<TArray>(result);
}
public bool ContainsKey(TKey key)
private TArray Self<TArray>() where TArray : ArrayDictionary<TKey, TValue>
{
for (var i = 0; i < items.Length; i++)
return (this as TArray)!;
}
private TArray Create<TArray>(KeyValuePair<TKey, TValue>[] newItems) where TArray : ArrayDictionary<TKey, TValue>
{
if (ReferenceEquals(items, newItems))
{
if (keyComparer.Equals(items[i].Key, key))
{
return true;
}
return Self<TArray>();
}
return false;
var newClone = (TArray)MemberwiseClone();
newClone.items = newItems;
return newClone;
}
public bool ContainsKey(TKey key)
{
var index = IndexOf(key);
return index >= 0;
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
var index = IndexOf(key);
if (index >= 0)
{
value = items[index].Value;
return true;
}
else
{
value = default!;
return false;
}
}
private int IndexOf(TKey key)
{
for (var i = 0; i < items.Length; i++)
{
if (keyComparer.Equals(items[i].Key, key))
{
value = items[i].Value;
return true;
return i;
}
}
value = default!;
return false;
return -1;
}
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()

17
backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs

@ -32,12 +32,21 @@ namespace Squidex.Infrastructure.Commands
this.store = store;
}
protected sealed override void ApplyEvent(Envelope<IEvent> @event)
protected sealed override bool ApplyEvent(Envelope<IEvent> @event, bool isLoading)
{
var newVersion = Version + 1;
snapshot = OnEvent(@event);
snapshot.Version = newVersion;
var newSnapshot = OnEvent(@event);
if (!ReferenceEquals(Snapshot, newSnapshot) || isLoading)
{
snapshot = newSnapshot;
snapshot.Version = newVersion;
return true;
}
return false;
}
protected sealed override void RestorePreviousSnapshot(T previousSnapshot, long previousVersion)
@ -47,7 +56,7 @@ namespace Squidex.Infrastructure.Commands
protected sealed override Task ReadAsync(Type type, Guid id)
{
persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot<T>(ApplySnapshot), ApplyEvent);
persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, new HandleSnapshot<T>(ApplySnapshot), x => ApplyEvent(x, true));
return persistence.ReadAsync();
}

9
backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -73,9 +73,10 @@ namespace Squidex.Infrastructure.Commands
@event.SetAggregateId(id);
ApplyEvent(@event);
uncomittedEvents.Add(@event);
if (ApplyEvent(@event, false))
{
uncomittedEvents.Add(@event);
}
}
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents()
@ -206,7 +207,7 @@ namespace Squidex.Infrastructure.Commands
protected abstract void RestorePreviousSnapshot(T previousSnapshot, long previousVersion);
protected abstract void ApplyEvent(Envelope<IEvent> @event);
protected abstract bool ApplyEvent(Envelope<IEvent> @event, bool isLoading);
protected abstract Task ReadAsync(Type type, Guid id);

17
backend/src/Squidex.Infrastructure/Commands/LogSnapshotDomainObjectGrain.cs

@ -54,17 +54,26 @@ namespace Squidex.Infrastructure.Commands
return default!;
}
protected sealed override void ApplyEvent(Envelope<IEvent> @event)
protected sealed override bool ApplyEvent(Envelope<IEvent> @event, bool isLoading)
{
var newVersion = Version + 1;
var snapshot = OnEvent(@event);
snapshot.Version = Version + 1;
snapshots.Add(snapshot);
if (!ReferenceEquals(Snapshot, snapshot) || isLoading)
{
snapshot.Version = newVersion;
snapshots.Add(snapshot);
return true;
}
return false;
}
protected sealed override Task ReadAsync(Type type, Guid id)
{
persistence = store.WithEventSourcing(type, id, ApplyEvent);
persistence = store.WithEventSourcing(type, id, x => ApplyEvent(x, true));
return persistence.ReadAsync();
}

3
backend/src/Squidex.Infrastructure/Json/Newtonsoft/JsonValueConverter.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Objects;
@ -103,7 +102,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft
case JsonToken.Boolean:
return JsonValue.Create((bool)reader.Value!);
case JsonToken.Date:
return JsonValue.Create(((DateTime)reader.Value!).ToString("yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture));
return JsonValue.Create(((DateTime)reader.Value!).ToIso8601());
case JsonToken.String:
return JsonValue.Create(reader.Value!.ToString());
case JsonToken.Null:

3
backend/src/Squidex.Infrastructure/Json/Objects/IJsonValue.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Json.Objects
{
@ -13,6 +14,8 @@ namespace Squidex.Infrastructure.Json.Objects
{
JsonValueType Type { get; }
bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result);
string ToJsonString();
string ToString();

18
backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs

@ -8,6 +8,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
namespace Squidex.Infrastructure.Json.Objects
@ -92,5 +94,21 @@ namespace Squidex.Infrastructure.Json.Objects
{
return $"[{string.Join(", ", this.Select(x => x.ToJsonString()))}]";
}
public bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result)
{
Guard.NotNull(pathSegment);
if (pathSegment != null && int.TryParse(pathSegment, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) && index >= 0 && index < Count)
{
result = this[index];
return true;
}
result = null!;
return false;
}
}
}

8
backend/src/Squidex.Infrastructure/Json/Objects/JsonNull.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Json.Objects
{
@ -51,5 +52,12 @@ namespace Squidex.Infrastructure.Json.Objects
{
return "null";
}
public bool TryGet(string pathSegment, [MaybeNullWhen(false)] out IJsonValue result)
{
result = null!;
return false;
}
}
}

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

Loading…
Cancel
Save