Browse Source

Refactoring/simplify immutability (#693)

* Simplify immutability.

* More cleanup.

* Code cleanup.

* Fix.

* More simplifications.

* Just a spelling fix.
pull/697/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
389b0dadaa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
  2. 4
      backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
  3. 2
      backend/extensions/Squidex.Extensions/Actions/Comment/CommentAction.cs
  4. 2
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs
  5. 2
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
  6. 2
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  7. 2
      backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs
  8. 2
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
  9. 2
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs
  10. 2
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
  11. 2
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationAction.cs
  12. 2
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
  13. 2
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
  14. 2
      backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
  15. 2
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
  16. 4
      backend/src/Migrations/OldEvents/AppPatternAdded.cs
  17. 4
      backend/src/Migrations/OldEvents/AppPatternUpdated.cs
  18. 42
      backend/src/Migrations/OldEvents/SchemaCreated.cs
  19. 10
      backend/src/Migrations/OldEvents/ScriptsConfigured.cs
  20. 2
      backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs
  21. 21
      backend/src/Migrations/OldTriggers/ContentChangedTrigger.cs
  22. 3
      backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs
  23. 28
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  24. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  25. 8
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs
  26. 33
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsSurrogate.cs
  27. 33
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsSurrogate.cs
  28. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguageConfigSurrogate.cs
  29. 23
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs
  30. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  31. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  32. 24
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  33. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs
  34. 28
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowsSurrogate.cs
  35. 22
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs
  36. 49
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  37. 15
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs
  38. 30
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
  39. 22
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs
  40. 34
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  41. 6
      backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml
  42. 27
      backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd
  43. 39
      backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs
  44. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Named.cs
  45. 69
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
  46. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  47. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
  48. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs
  49. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs
  50. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchemaV2.cs
  51. 21
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
  52. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs
  53. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs
  54. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/UsageTrigger.cs
  55. 25
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  56. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
  57. 44
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  58. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  59. 15
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
  60. 23
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldBase.cs
  61. 29
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  62. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs
  63. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  64. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs
  65. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs
  66. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
  67. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
  68. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs
  69. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs
  70. 77
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs
  71. 27
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
  72. 21
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  73. 36
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  74. 85
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs
  75. 25
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
  76. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  77. 17
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  78. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs
  79. 37
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  80. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
  81. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs
  82. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  83. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs
  84. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  85. 10
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs
  86. 10
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs
  87. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs
  88. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs
  89. 17
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs
  90. 6
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/ReferencesFieldBuilder.cs
  91. 10
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs
  92. 39
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs
  93. 7
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs
  94. 5
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs
  95. 42
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs
  96. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs
  97. 2
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs
  98. 19
      backend/src/Squidex.Infrastructure/Cloneable{T}.cs
  99. 129
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  100. 45
      backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary.cs

2
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs

@ -19,7 +19,7 @@ namespace Squidex.Extensions.Actions.Algolia
Display = "Populate Algolia index", Display = "Populate Algolia index",
Description = "Populate a full text search index in Algolia.", Description = "Populate a full text search index in Algolia.",
ReadMore = "https://www.algolia.com/")] ReadMore = "https://www.algolia.com/")]
public sealed class AlgoliaAction : RuleAction public sealed record AlgoliaAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Application Id", Description = "The application ID.")] [Display(Name = "Application Id", Description = "The application ID.")]

4
backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -21,7 +21,7 @@ namespace Squidex.Extensions.Actions.AzureQueue
Display = "Send to Azure Queue", Display = "Send to Azure Queue",
Description = "Send an event to azure queue storage.", Description = "Send an event to azure queue storage.",
ReadMore = "https://azure.microsoft.com/en-us/services/storage/queues/")] ReadMore = "https://azure.microsoft.com/en-us/services/storage/queues/")]
public sealed class AzureQueueAction : RuleAction public sealed record AzureQueueAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Connection", Description = "The connection string to the storage account.")] [Display(Name = "Connection", Description = "The connection string to the storage account.")]

2
backend/extensions/Squidex.Extensions/Actions/Comment/CommentAction.cs

@ -18,7 +18,7 @@ namespace Squidex.Extensions.Actions.Comment
IconColor = "#3389ff", IconColor = "#3389ff",
Display = "Create comment", Display = "Create comment",
Description = "Create a comment for a content event.")] Description = "Create a comment for a content event.")]
public sealed class CommentAction : RuleAction public sealed record CommentAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Text", Description = "The comment text.")] [Display(Name = "Text", Description = "The comment text.")]

2
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs

@ -18,7 +18,7 @@ namespace Squidex.Extensions.Actions.CreateContent
IconColor = "#3389ff", IconColor = "#3389ff",
Display = "Create content", Display = "Create content",
Description = "Create a a new content item for any schema.")] Description = "Create a a new content item for any schema.")]
public sealed class CreateContentAction : RuleAction public sealed record CreateContentAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Data", Description = "The content data.")] [Display(Name = "Data", Description = "The content data.")]

2
backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs

@ -20,7 +20,7 @@ namespace Squidex.Extensions.Actions.Discourse
Display = "Post to discourse", Display = "Post to discourse",
Description = "Create a post or topic at discourse.", Description = "Create a post or topic at discourse.",
ReadMore = "https://www.discourse.org/")] ReadMore = "https://www.discourse.org/")]
public sealed class DiscourseAction : RuleAction public sealed record DiscourseAction : RuleAction
{ {
[AbsoluteUrl] [AbsoluteUrl]
[LocalizedRequired] [LocalizedRequired]

2
backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs

@ -20,7 +20,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch
Display = "Populate Elasticsearch index", Display = "Populate Elasticsearch index",
Description = "Populate a full text search index in ElasticSearch.", Description = "Populate a full text search index in ElasticSearch.",
ReadMore = "https://www.elastic.co/")] ReadMore = "https://www.elastic.co/")]
public sealed class ElasticSearchAction : RuleAction public sealed record ElasticSearchAction : RuleAction
{ {
[AbsoluteUrl] [AbsoluteUrl]
[LocalizedRequired] [LocalizedRequired]

2
backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs

@ -19,7 +19,7 @@ namespace Squidex.Extensions.Actions.Email
Display = "Send an email", Display = "Send an email",
Description = "Send an email with a custom SMTP server.", Description = "Send an email with a custom SMTP server.",
ReadMore = "https://en.wikipedia.org/wiki/Email")] ReadMore = "https://en.wikipedia.org/wiki/Email")]
public sealed class EmailAction : RuleAction public sealed record EmailAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Server Host", Description = "The IP address or host to the SMTP server.")] [Display(Name = "Server Host", Description = "The IP address or host to the SMTP server.")]

2
backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs

@ -19,7 +19,7 @@ namespace Squidex.Extensions.Actions.Fastly
Display = "Purge fastly cache", Display = "Purge fastly cache",
Description = "Remove entries from the fastly CDN cache.", Description = "Remove entries from the fastly CDN cache.",
ReadMore = "https://www.fastly.com/")] ReadMore = "https://www.fastly.com/")]
public sealed class FastlyAction : RuleAction public sealed record FastlyAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")] [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")]

2
backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs

@ -19,7 +19,7 @@ namespace Squidex.Extensions.Actions.Kafka
Display = "Push to kafka", Display = "Push to kafka",
Description = "Connect to Kafka stream and push data to that stream.", Description = "Connect to Kafka stream and push data to that stream.",
ReadMore = "https://kafka.apache.org/quickstart")] ReadMore = "https://kafka.apache.org/quickstart")]
public sealed class KafkaAction : RuleAction public sealed record KafkaAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Topic Name", Description = "The name of the topic.")] [Display(Name = "Topic Name", Description = "The name of the topic.")]

2
backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs

@ -19,7 +19,7 @@ namespace Squidex.Extensions.Actions.Medium
Display = "Post to Medium", Display = "Post to Medium",
Description = "Create a new story or post at medium.", Description = "Create a new story or post at medium.",
ReadMore = "https://medium.com/")] ReadMore = "https://medium.com/")]
public sealed class MediumAction : RuleAction public sealed record MediumAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Access Token", Description = "The self issued access token.")] [Display(Name = "Access Token", Description = "The self issued access token.")]

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

@ -18,7 +18,7 @@ namespace Squidex.Extensions.Actions.Notification
IconColor = "#3389ff", IconColor = "#3389ff",
Display = "Send a notification", Display = "Send a notification",
Description = "Send an integrated notification to a user.")] Description = "Send an integrated notification to a user.")]
public sealed class NotificationAction : RuleAction public sealed record NotificationAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "User", Description = "The user id or email.")] [Display(Name = "User", Description = "The user id or email.")]

2
backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs

@ -19,7 +19,7 @@ namespace Squidex.Extensions.Actions.Prerender
Display = "Recache URL", Display = "Recache URL",
Description = "Prerender a javascript website for bots.", Description = "Prerender a javascript website for bots.",
ReadMore = "https://prerender.io")] ReadMore = "https://prerender.io")]
public sealed class PrerenderAction : RuleAction public sealed record PrerenderAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Token", Description = "The prerender token from your account.")] [Display(Name = "Token", Description = "The prerender token from your account.")]

2
backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs

@ -20,7 +20,7 @@ namespace Squidex.Extensions.Actions.Slack
Display = "Send to Slack", Display = "Send to Slack",
Description = "Create a status update to a slack channel.", Description = "Create a status update to a slack channel.",
ReadMore = "https://slack.com")] ReadMore = "https://slack.com")]
public sealed class SlackAction : RuleAction public sealed record SlackAction : RuleAction
{ {
[AbsoluteUrl] [AbsoluteUrl]
[LocalizedRequired] [LocalizedRequired]

2
backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs

@ -19,7 +19,7 @@ namespace Squidex.Extensions.Actions.Twitter
Display = "Tweet", Display = "Tweet",
Description = "Tweet an update with your twitter account.", Description = "Tweet an update with your twitter account.",
ReadMore = "https://twitter.com")] ReadMore = "https://twitter.com")]
public sealed class TweetAction : RuleAction public sealed record TweetAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Access Token", Description = " The generated access token.")] [Display(Name = "Access Token", Description = " The generated access token.")]

2
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs

@ -20,7 +20,7 @@ namespace Squidex.Extensions.Actions.Webhook
Display = "Send webhook", Display = "Send webhook",
Description = "Invoke HTTP endpoints on a target system.", Description = "Invoke HTTP endpoints on a target system.",
ReadMore = "https://en.wikipedia.org/wiki/Webhook")] ReadMore = "https://en.wikipedia.org/wiki/Webhook")]
public sealed class WebhookAction : RuleAction public sealed record WebhookAction : RuleAction
{ {
[LocalizedRequired] [LocalizedRequired]
[Display(Name = "Url", Description = "The url to the webhook.")] [Display(Name = "Url", Description = "The url to the webhook.")]

4
backend/src/Migrations/OldEvents/AppPatternAdded.cs

@ -36,13 +36,13 @@ namespace Migrations.OldEvents
{ {
var newSettings = new AppSettings var newSettings = new AppSettings
{ {
Patterns = new List<Pattern>(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) Patterns = ImmutableList.ToImmutableList(new List<Pattern>(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern))
{ {
new Pattern(Name, Pattern) new Pattern(Name, Pattern)
{ {
Message = Message Message = Message
} }
}.ToReadOnlyCollection(), }),
Editors = state.Settings.Editors Editors = state.Settings.Editors
}; };

4
backend/src/Migrations/OldEvents/AppPatternUpdated.cs

@ -36,13 +36,13 @@ namespace Migrations.OldEvents
{ {
var newSettings = new AppSettings var newSettings = new AppSettings
{ {
Patterns = new List<Pattern>(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) Patterns = ImmutableList.ToImmutableList(new List<Pattern>(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern))
{ {
new Pattern(Name, Pattern) new Pattern(Name, Pattern)
{ {
Message = Message Message = Message
} }
}.ToReadOnlyCollection(), }),
Editors = state.Settings.Editors Editors = state.Settings.Editors
}; };

42
backend/src/Migrations/OldEvents/SchemaCreated.cs

@ -50,7 +50,11 @@ namespace Migrations.OldEvents
var partitioning = Partitioning.FromString(eventField.Partitioning); var partitioning = Partitioning.FromString(eventField.Partitioning);
var field = eventField.Properties.CreateRootField(totalFields, eventField.Name, partitioning); var field =
eventField.Properties.CreateRootField(
totalFields,
eventField.Name, partitioning,
eventField);
if (field is ArrayField arrayField && eventField.Nested?.Length > 0) if (field is ArrayField arrayField && eventField.Nested?.Length > 0)
{ {
@ -58,22 +62,11 @@ namespace Migrations.OldEvents
{ {
totalFields++; totalFields++;
var nestedField = nestedEventField.Properties.CreateNestedField(totalFields, nestedEventField.Name); var nestedField =
nestedEventField.Properties.CreateNestedField(
if (nestedEventField.IsHidden) totalFields,
{ nestedEventField.Name,
nestedField = nestedField.Hide(); nestedEventField);
}
if (nestedEventField.IsDisabled)
{
nestedField = nestedField.Disable();
}
if (nestedEventField.IsLocked)
{
nestedField = nestedField.Lock();
}
arrayField = arrayField.AddField(nestedField); arrayField = arrayField.AddField(nestedField);
} }
@ -81,21 +74,6 @@ namespace Migrations.OldEvents
field = arrayField; field = arrayField;
} }
if (eventField.IsHidden)
{
field = field.Hide();
}
if (eventField.IsDisabled)
{
field = field.Disable();
}
if (eventField.IsLocked)
{
field = field.Lock();
}
schema = schema.AddField(field); schema = schema.AddField(field);
} }
} }

10
backend/src/Migrations/OldEvents/ScriptsConfigured.cs

@ -35,27 +35,27 @@ namespace Migrations.OldEvents
if (!string.IsNullOrWhiteSpace(ScriptQuery)) if (!string.IsNullOrWhiteSpace(ScriptQuery))
{ {
scripts.Query = ScriptQuery; scripts = scripts with { Query = ScriptQuery };
} }
if (!string.IsNullOrWhiteSpace(ScriptCreate)) if (!string.IsNullOrWhiteSpace(ScriptCreate))
{ {
scripts.Create = ScriptCreate; scripts = scripts with { Create = ScriptCreate };
} }
if (!string.IsNullOrWhiteSpace(ScriptUpdate)) if (!string.IsNullOrWhiteSpace(ScriptUpdate))
{ {
scripts.Update = ScriptUpdate; scripts = scripts with { Update = ScriptUpdate };
} }
if (!string.IsNullOrWhiteSpace(ScriptDelete)) if (!string.IsNullOrWhiteSpace(ScriptDelete))
{ {
scripts.Delete = ScriptDelete; scripts = scripts with { Delete = ScriptDelete };
} }
if (!string.IsNullOrWhiteSpace(ScriptChange)) if (!string.IsNullOrWhiteSpace(ScriptChange))
{ {
scripts.Change = ScriptChange; scripts = scripts with { Change = ScriptChange };
} }
return SimpleMapper.Map(this, new SchemaScriptsConfigured { Scripts = scripts }); return SimpleMapper.Map(this, new SchemaScriptsConfigured { Scripts = scripts });

2
backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection;
namespace Migrations.OldTriggers namespace Migrations.OldTriggers
{ {
[TypeName(nameof(AssetChangedTrigger))] [TypeName(nameof(AssetChangedTrigger))]
public sealed class AssetChangedTrigger : RuleTrigger, IMigrated<RuleTrigger> public sealed record AssetChangedTrigger : RuleTrigger, IMigrated<RuleTrigger>
{ {
public bool SendCreate { get; set; } public bool SendCreate { get; set; }

21
backend/src/Migrations/OldTriggers/ContentChangedTrigger.cs

@ -6,19 +6,19 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Migrations; using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Migrations.OldTriggers namespace Migrations.OldTriggers
{ {
[TypeName(nameof(ContentChangedTrigger))] [TypeName(nameof(ContentChangedTrigger))]
public sealed class ContentChangedTrigger : RuleTrigger, IMigrated<RuleTrigger> public sealed record ContentChangedTrigger : RuleTrigger, IMigrated<RuleTrigger>
{ {
public ReadOnlyCollection<ContentChangedTriggerSchema> Schemas { get; set; } public ImmutableList<ContentChangedTriggerSchema> Schemas { get; set; }
public bool HandleAll { get; set; } public bool HandleAll { get; set; }
@ -27,22 +27,9 @@ namespace Migrations.OldTriggers
throw new NotSupportedException(); throw new NotSupportedException();
} }
public override void Freeze()
{
base.Freeze();
if (Schemas != null)
{
foreach (var schema in Schemas)
{
schema.Freeze();
}
}
}
public RuleTrigger Migrate() public RuleTrigger Migrate()
{ {
var schemas = new ReadOnlyCollection<ContentChangedTriggerSchemaV2>(Schemas.Select(x => x.Migrate()).ToList()); var schemas = Schemas.Select(x => x.Migrate()).ToImmutableList();
return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas }; return new ContentChangedTriggerV2 { HandleAll = HandleAll, Schemas = schemas };
} }

3
backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs

@ -7,14 +7,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Migrations.OldTriggers namespace Migrations.OldTriggers
{ {
public sealed class ContentChangedTriggerSchema : Freezable public sealed class ContentChangedTriggerSchema
{ {
public Guid SchemaId { get; set; } public Guid SchemaId { get; set; }

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

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
} }
public AppClients(Dictionary<string, AppClient> inner) public AppClients(IDictionary<string, AppClient> inner)
: base(inner) : base(inner)
{ {
} }
@ -30,7 +30,12 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNullOrEmpty(id, nameof(id));
return Without<AppClients>(id); if (!this.TryRemove(id, out var updated))
{
return this;
}
return new AppClients(updated);
} }
[Pure] [Pure]
@ -39,17 +44,17 @@ namespace Squidex.Domain.Apps.Core.Apps
Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNullOrEmpty(id, nameof(id));
Guard.NotNullOrEmpty(secret, nameof(secret)); Guard.NotNullOrEmpty(secret, nameof(secret));
if (ContainsKey(id))
{
return this;
}
var newClient = new AppClient(id, secret) var newClient = new AppClient(id, secret)
{ {
Role = role.Or(Role.Editor) Role = role.Or(Role.Editor)
}; };
return With<AppClients>(id, newClient); if (!this.TryAdd(id, newClient, out var updated))
{
return this;
}
return new AppClients(updated);
} }
[Pure] [Pure]
@ -90,7 +95,12 @@ namespace Squidex.Domain.Apps.Core.Apps
newClient = newClient with { ApiTrafficLimit = apiTrafficLimit.Value }; newClient = newClient with { ApiTrafficLimit = apiTrafficLimit.Value };
} }
return With<AppClients>(id, newClient); if (!this.TrySet(id, newClient, out var updated))
{
return this;
}
return new AppClients(updated);
} }
} }
} }

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

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
} }
public AppContributors(Dictionary<string, string> inner) public AppContributors(IDictionary<string, string> inner)
: base(inner) : base(inner)
{ {
} }
@ -31,7 +31,12 @@ namespace Squidex.Domain.Apps.Core.Apps
Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); Guard.NotNullOrEmpty(contributorId, nameof(contributorId));
Guard.NotNullOrEmpty(role, nameof(role)); Guard.NotNullOrEmpty(role, nameof(role));
return With<AppContributors>(contributorId, role, EqualityComparer<string>.Default); if (!this.TrySet(contributorId, role, out var updated))
{
return this;
}
return new AppContributors(updated);
} }
[Pure] [Pure]
@ -39,7 +44,12 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); Guard.NotNullOrEmpty(contributorId, nameof(contributorId));
return Without<AppContributors>(contributorId); if (!this.TryRemove(contributorId, out var updated))
{
return this;
}
return new AppContributors(updated);
} }
} }
} }

8
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppSettings.cs

@ -5,19 +5,17 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record AppSettings
public sealed class AppSettings
{ {
public static readonly AppSettings Empty = new AppSettings(); public static readonly AppSettings Empty = new AppSettings();
public ReadOnlyCollection<Pattern> Patterns { get; init; } = ReadOnlyCollection.Empty<Pattern>(); public ImmutableList<Pattern> Patterns { get; init; } = ImmutableList.Empty<Pattern>();
public ReadOnlyCollection<Editor> Editors { get; init; } = ReadOnlyCollection.Empty<Editor>(); public ImmutableList<Editor> Editors { get; init; } = ImmutableList.Empty<Editor>();
public bool HideScheduler { get; init; } public bool HideScheduler { get; init; }

33
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsSurrogate.cs

@ -1,33 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class AppClientsSurrogate : Dictionary<string, AppClient>, ISurrogate<AppClients>
{
public void FromSource(AppClients source)
{
foreach (var (key, client) in source)
{
Add(key, client);
}
}
public AppClients ToSource()
{
if (Count == 0)
{
return AppClients.Empty;
}
return new AppClients(this);
}
}
}

33
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsSurrogate.cs

@ -1,33 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class AppContributorsSurrogate : Dictionary<string, string>, ISurrogate<AppContributors>
{
public void FromSource(AppContributors source)
{
foreach (var (userId, role) in source)
{
Add(userId, role);
}
}
public AppContributors ToSource()
{
if (Count == 0)
{
return AppContributors.Empty;
}
return new AppContributors(this);
}
}
}

3
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguageConfigSurrogate.cs

@ -7,6 +7,7 @@
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps.Json namespace Squidex.Domain.Apps.Core.Apps.Json
{ {
@ -31,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
} }
else else
{ {
return new LanguageConfig(IsOptional, Fallback); return new LanguageConfig(IsOptional, ImmutableList.Create(Fallback));
} }
} }
} }

23
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs

@ -5,39 +5,36 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record LanguageConfig
public sealed class LanguageConfig
{ {
public static readonly LanguageConfig Default = new LanguageConfig(); public static readonly LanguageConfig Default = new LanguageConfig();
private readonly Language[] fallbacks;
public bool IsOptional { get; } public bool IsOptional { get; }
public IEnumerable<Language> Fallbacks public ImmutableList<Language> Fallbacks { get; } = ImmutableList.Empty<Language>();
{
get => fallbacks;
}
public LanguageConfig(bool isOptional = false, params Language[]? fallbacks) public LanguageConfig(bool isOptional = false, ImmutableList<Language>? fallbacks = null)
{ {
IsOptional = isOptional; IsOptional = isOptional;
this.fallbacks = fallbacks ?? Array.Empty<Language>(); if (fallbacks != null)
{
Fallbacks = fallbacks;
}
} }
internal LanguageConfig Cleanup(string self, IReadOnlyDictionary<string, LanguageConfig> allowed) internal LanguageConfig Cleanup(string self, IReadOnlyDictionary<string, LanguageConfig> allowed)
{ {
if (fallbacks.Any(x => x.Iso2Code == self) || fallbacks.Any(x => !allowed.ContainsKey(x))) if (Fallbacks.Any(x => x.Iso2Code == self) || Fallbacks.Any(x => !allowed.ContainsKey(x)))
{ {
var cleaned = Fallbacks.Where(x => x.Iso2Code != self && allowed.ContainsKey(x.Iso2Code)).ToArray(); var cleaned = Fallbacks.Where(x => x.Iso2Code != self && allowed.ContainsKey(x.Iso2Code)).ToImmutableList();
return new LanguageConfig(IsOptional, cleaned); return new LanguageConfig(IsOptional, cleaned);
} }

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

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
@ -66,7 +67,7 @@ namespace Squidex.Domain.Apps.Core.Apps
var newLanguages = new Dictionary<string, LanguageConfig>(languages) var newLanguages = new Dictionary<string, LanguageConfig>(languages)
{ {
[language] = new LanguageConfig(isOptional, fallbacks) [language] = new LanguageConfig(isOptional, ImmutableList.Create(fallbacks))
}; };
return Build(newLanguages, master); return Build(newLanguages, master);

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

@ -15,8 +15,7 @@ using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record Role : Named
public sealed class Role : Named
{ {
private static readonly HashSet<string> ExtraPermissions = new HashSet<string> private static readonly HashSet<string> ExtraPermissions = new HashSet<string>
{ {
@ -41,7 +40,6 @@ namespace Squidex.Domain.Apps.Core.Apps
public JsonObject Properties { get; } public JsonObject Properties { get; }
[IgnoreDuringEquals]
public bool IsDefault public bool IsDefault
{ {
get => Roles.IsDefault(this); get => Roles.IsDefault(this);

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

@ -92,25 +92,30 @@ namespace Squidex.Domain.Apps.Core.Apps
[Pure] [Pure]
public Roles Remove(string name) public Roles Remove(string name)
{ {
return Create(inner.Without(name)); if (!inner.TryRemove(name, out var updated))
{
return this;
}
return Create(new ImmutableDictionary<string, Role>(updated));
} }
[Pure] [Pure]
public Roles Add(string name) public Roles Add(string name)
{ {
if (inner.ContainsKey(name)) if (IsDefault(name))
{ {
return this; return this;
} }
if (IsDefault(name)) var newRole = Role.Create(name);
if (!inner.TryAdd(name, newRole, out var updated))
{ {
return this; return this;
} }
var newRole = Role.Create(name); return Create(new ImmutableDictionary<string, Role>(updated));
return Create(inner.With(name, newRole));
} }
[Pure] [Pure]
@ -125,7 +130,12 @@ namespace Squidex.Domain.Apps.Core.Apps
var newRole = role.Update(permissions, properties); var newRole = role.Update(permissions, properties);
return Create(inner.With(name, newRole)); if (!inner.TrySet(name, newRole, out var updated))
{
return this;
}
return Create(new ImmutableDictionary<string, Role>(updated));
} }
public static bool IsDefault(string role) public static bool IsDefault(string role)

3
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowStepSurrogate.cs

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Contents.Json namespace Squidex.Domain.Apps.Core.Contents.Json
@ -49,7 +50,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
} }
var transitions = var transitions =
Transitions?.ToDictionary( Transitions?.ToImmutableDictionary(
x => x.Key, x => x.Key,
x => x.Value.ToSource()); x => x.Value.ToSource());

28
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowsSurrogate.cs

@ -1,28 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents.Json
{
public sealed class WorkflowsSurrogate : Dictionary<DomainId, Workflow>, ISurrogate<Workflows>
{
public void FromSource(Workflows source)
{
foreach (var (key, workflow) in source)
{
Add(key, workflow);
}
}
public Workflows ToSource()
{
return new Workflows(this);
}
}
}

22
backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs

@ -5,35 +5,27 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record NoUpdate : WorkflowCondition
public sealed class NoUpdate : WorkflowCondition
{ {
public static readonly NoUpdate Always = new NoUpdate(null, null); public static readonly NoUpdate Always = new NoUpdate();
public NoUpdate(string? expression, ReadOnlyCollection<string>? roles)
: base(expression, roles)
{
}
public static NoUpdate When(string? expression, params string[]? roles) public static NoUpdate When(string? expression, params string[]? roles)
{ {
if (roles?.Length > 0) if (roles?.Length > 0)
{ {
return new NoUpdate(expression, ReadOnlyCollection.Create(roles)); return new NoUpdate { Expression = expression, Roles = roles?.ToImmutableList() };
} }
else if (!string.IsNullOrWhiteSpace(expression))
if (!string.IsNullOrWhiteSpace(expression))
{ {
return new NoUpdate(expression, null); return new NoUpdate { Expression = expression };
} }
else
{
return Always; return Always;
} }
} }
}
} }

49
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -6,38 +6,32 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
#pragma warning disable IDE0051 // Remove unused private members
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record Workflow
public sealed class Workflow : Named
{ {
private const string DefaultName = "Unnamed"; private const string DefaultName = "Unnamed";
public static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
public static readonly Workflow Default = CreateDefault(); public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(default, null); public static readonly Workflow Empty = new Workflow(default, null);
[IgnoreDuringEquals] public Status Initial { get; }
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps;
public ImmutableDictionary<Status, WorkflowStep> Steps { get; } = ImmutableDictionary.Empty<Status, WorkflowStep>();
public ReadOnlyCollection<DomainId> SchemaIds { get; } = ReadOnlyCollection.Empty<DomainId>(); public ImmutableList<DomainId> SchemaIds { get; } = ImmutableList.Empty<DomainId>();
public Status Initial { get; } public string Name { get; }
public Workflow( public Workflow(
Status initial, Status initial,
IReadOnlyDictionary<Status, WorkflowStep>? steps = null, ImmutableDictionary<Status, WorkflowStep>? steps = null,
IReadOnlyList<DomainId>? schemaIds = null, ImmutableList<DomainId>? schemaIds = null,
string? name = null) string? name = null)
: base(name.Or(DefaultName))
{ {
Initial = initial; Initial = initial;
@ -48,21 +42,24 @@ namespace Squidex.Domain.Apps.Core.Contents
if (schemaIds != null) if (schemaIds != null)
{ {
SchemaIds = schemaIds.ToReadOnlyCollection(); SchemaIds = schemaIds;
} }
Name = name.Or(DefaultName);
} }
public static Workflow CreateDefault(string? name = null) public static Workflow CreateDefault(string? name = null)
{ {
return new Workflow( return new Workflow(
Status.Draft, new Dictionary<Status, WorkflowStep> Status.Draft,
new Dictionary<Status, WorkflowStep>
{ {
[Status.Archived] = [Status.Archived] =
new WorkflowStep( new WorkflowStep(
new Dictionary<Status, WorkflowTransition> new Dictionary<Status, WorkflowTransition>
{ {
[Status.Draft] = WorkflowTransition.Always [Status.Draft] = WorkflowTransition.Always
}, }.ToImmutableDictionary(),
StatusColors.Archived, NoUpdate.Always), StatusColors.Archived, NoUpdate.Always),
[Status.Draft] = [Status.Draft] =
new WorkflowStep( new WorkflowStep(
@ -70,7 +67,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
[Status.Archived] = WorkflowTransition.Always, [Status.Archived] = WorkflowTransition.Always,
[Status.Published] = WorkflowTransition.Always [Status.Published] = WorkflowTransition.Always
}, }.ToImmutableDictionary(),
StatusColors.Draft), StatusColors.Draft),
[Status.Published] = [Status.Published] =
new WorkflowStep( new WorkflowStep(
@ -78,9 +75,9 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
[Status.Archived] = WorkflowTransition.Always, [Status.Archived] = WorkflowTransition.Always,
[Status.Draft] = WorkflowTransition.Always [Status.Draft] = WorkflowTransition.Always
}, }.ToImmutableDictionary(),
StatusColors.Published) StatusColors.Published)
}, null, name); }.ToImmutableDictionary(), null, name);
} }
public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status)
@ -128,17 +125,5 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
return (Initial, Steps[Initial]); return (Initial, Steps[Initial]);
} }
[CustomEqualsInternal]
private bool CustomEquals(Workflow other)
{
return Steps.EqualsDictionary(other.Steps);
}
[CustomGetHashCode]
private int CustomHashCode()
{
return Steps.DictionaryHashCode();
}
} }
} }

15
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs

@ -5,21 +5,14 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
public abstract class WorkflowCondition public abstract record WorkflowCondition
{ {
public string? Expression { get; } public string? Expression { get; init; }
public ReadOnlyCollection<string>? Roles { get; } public ImmutableList<string>? Roles { get; init; }
protected WorkflowCondition(string? expression, ReadOnlyCollection<string>? roles)
{
Expression = expression;
Roles = roles;
}
} }
} }

30
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs

@ -5,44 +5,28 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure;
#pragma warning disable IDE0051 // Remove unused private members
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record WorkflowStep
public sealed class WorkflowStep
{ {
private static readonly IReadOnlyDictionary<Status, WorkflowTransition> EmptyTransitions = new Dictionary<Status, WorkflowTransition>(); public ImmutableDictionary<Status, WorkflowTransition> Transitions { get; } = ImmutableDictionary.Empty<Status, WorkflowTransition>();
[IgnoreDuringEquals]
public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; }
public string? Color { get; } public string? Color { get; }
public NoUpdate? NoUpdate { get; } public NoUpdate? NoUpdate { get; }
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition>? transitions = null, string? color = null, NoUpdate? noUpdate = null) public WorkflowStep(ImmutableDictionary<Status, WorkflowTransition>? transitions = null, string? color = null, NoUpdate? noUpdate = null)
{ {
Transitions = transitions ?? EmptyTransitions;
Color = color; Color = color;
NoUpdate = noUpdate; if (transitions != null)
}
[CustomEqualsInternal]
private bool CustomEquals(WorkflowStep other)
{ {
return Transitions.EqualsDictionary(other.Transitions); Transitions = transitions;
} }
[CustomGetHashCode] NoUpdate = noUpdate;
private int CustomHashCode()
{
return Transitions.DictionaryHashCode();
} }
} }
} }

22
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs

@ -5,35 +5,27 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record WorkflowTransition : WorkflowCondition
public sealed class WorkflowTransition : WorkflowCondition
{ {
public static readonly WorkflowTransition Always = new WorkflowTransition(null, null); public static readonly WorkflowTransition Always = new WorkflowTransition();
public WorkflowTransition(string? expression, ReadOnlyCollection<string>? roles)
: base(expression, roles)
{
}
public static WorkflowTransition When(string? expression, params string[]? roles) public static WorkflowTransition When(string? expression, params string[]? roles)
{ {
if (roles?.Length > 0) if (roles?.Length > 0)
{ {
return new WorkflowTransition(expression, ReadOnlyCollection.Create(roles)); return new WorkflowTransition { Expression = expression, Roles = roles?.ToImmutableList() };
} }
else if (!string.IsNullOrWhiteSpace(expression))
if (!string.IsNullOrWhiteSpace(expression))
{ {
return new WorkflowTransition(expression, null); return new WorkflowTransition { Expression = expression };
} }
else
{
return Always; return Always;
} }
} }
}
} }

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

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
} }
public Workflows(Dictionary<DomainId, Workflow> inner) public Workflows(IDictionary<DomainId, Workflow> inner)
: base(inner) : base(inner)
{ {
} }
@ -29,7 +29,12 @@ namespace Squidex.Domain.Apps.Core.Contents
[Pure] [Pure]
public Workflows Remove(DomainId id) public Workflows Remove(DomainId id)
{ {
return Without<Workflows>(id); if (!this.TryRemove(id, out var updated))
{
return this;
}
return new Workflows(updated);
} }
[Pure] [Pure]
@ -37,7 +42,12 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNullOrEmpty(name, nameof(name)); Guard.NotNullOrEmpty(name, nameof(name));
return With<Workflows>(workflowId, Workflow.CreateDefault(name)); if (!this.TryAdd(workflowId, Workflow.CreateDefault(name), out var updated))
{
return this;
}
return new Workflows(updated);
} }
[Pure] [Pure]
@ -45,7 +55,12 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNull(workflow, nameof(workflow)); Guard.NotNull(workflow, nameof(workflow));
return With<Workflows>(default, workflow); if (!this.TrySet(default, workflow, out var updated))
{
return this;
}
return new Workflows(updated);
} }
[Pure] [Pure]
@ -53,7 +68,12 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
Guard.NotNull(workflow, nameof(workflow)); Guard.NotNull(workflow, nameof(workflow));
return With<Workflows>(id, workflow); if (!this.TrySet(id, workflow, out var updated))
{
return this;
}
return new Workflows(updated);
} }
[Pure] [Pure]
@ -66,12 +86,12 @@ namespace Squidex.Domain.Apps.Core.Contents
return Set(workflow); return Set(workflow);
} }
if (!ContainsKey(id)) if (!this.TryUpdate(id, workflow, out var updated))
{ {
return this; return this;
} }
return With<Workflows>(id, workflow); return new Workflows(updated);
} }
public Workflow GetFirst() public Workflow GetFirst()

6
backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<SquidexFreezable />
<Equals />
</Weavers>

27
backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="SquidexFreezable" minOccurs="0" maxOccurs="1" type="xs:anyType" />
<xs:element name="Equals" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

39
backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs

@ -1,39 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core
{
[Equals(DoNotAddEquals = true, DoNotAddGetHashCode = true, DoNotAddEqualityOperators = true)]
public abstract class Freezable : IFreezable
{
private bool isFrozen;
[IgnoreEquals]
[IgnoreDuringEquals]
public bool IsFrozen
{
get => isFrozen;
}
protected void CheckIfFrozen()
{
if (isFrozen)
{
throw new InvalidOperationException("Object is frozen");
}
}
public virtual void Freeze()
{
isFrozen = true;
}
}
}

2
backend/src/Squidex.Domain.Apps.Core.Model/Named.cs

@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core namespace Squidex.Domain.Apps.Core
{ {
public abstract class Named public abstract record Named
{ {
public string Name { get; } public string Name { get; }

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

@ -11,81 +11,65 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
{ {
public sealed class Rule : Cloneable<Rule> public sealed class Rule
{ {
private RuleTrigger trigger; public string? Name { get; private set; }
private RuleAction action;
private string name;
private bool isEnabled = true;
public string Name public RuleTrigger Trigger { get; private set; }
{
get => name;
}
public RuleTrigger Trigger public RuleAction Action { get; private set; }
{
get => trigger;
}
public RuleAction Action public bool IsEnabled { get; private set; } = true;
{
get => action;
}
public bool IsEnabled
{
get => isEnabled;
}
public Rule(RuleTrigger trigger, RuleAction action) public Rule(RuleTrigger trigger, RuleAction action)
{ {
Guard.NotNull(trigger, nameof(trigger)); Guard.NotNull(trigger, nameof(trigger));
Guard.NotNull(action, nameof(action)); Guard.NotNull(action, nameof(action));
SetTrigger(trigger); Action = action;
SetAction(action);
Trigger = trigger;
} }
[Pure] [Pure]
public Rule Rename(string newName) public Rule Rename(string newName)
{ {
if (string.Equals(name, newName)) if (string.Equals(Name, newName))
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.name = newName; clone.Name = newName;
}); });
} }
[Pure] [Pure]
public Rule Enable() public Rule Enable()
{ {
if (isEnabled) if (IsEnabled)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isEnabled = true; clone.IsEnabled = true;
}); });
} }
[Pure] [Pure]
public Rule Disable() public Rule Disable()
{ {
if (!isEnabled) if (!IsEnabled)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isEnabled = false; clone.IsEnabled = false;
}); });
} }
@ -94,19 +78,19 @@ namespace Squidex.Domain.Apps.Core.Rules
{ {
Guard.NotNull(newTrigger, nameof(newTrigger)); Guard.NotNull(newTrigger, nameof(newTrigger));
if (newTrigger.GetType() != trigger.GetType()) if (newTrigger.GetType() != Trigger.GetType())
{ {
throw new ArgumentException("New trigger has another type.", nameof(newTrigger)); throw new ArgumentException("New trigger has another type.", nameof(newTrigger));
} }
if (trigger.DeepEquals(newTrigger)) if (Trigger.Equals(newTrigger))
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.SetTrigger(newTrigger); clone.Trigger = newTrigger;
}); });
} }
@ -115,32 +99,29 @@ namespace Squidex.Domain.Apps.Core.Rules
{ {
Guard.NotNull(newAction, nameof(newAction)); Guard.NotNull(newAction, nameof(newAction));
if (newAction.GetType() != action.GetType()) if (newAction.GetType() != Action.GetType())
{ {
throw new ArgumentException("New action has another type.", nameof(newAction)); throw new ArgumentException("New action has another type.", nameof(newAction));
} }
if (action.DeepEquals(newAction)) if (Action.Equals(newAction))
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.SetAction(newAction); clone.Action = newAction;
}); });
} }
private void SetAction(RuleAction newAction) private Rule Clone(Action<Rule> updater)
{ {
action = newAction; var clone = (Rule)MemberwiseClone();
action.Freeze();
}
private void SetTrigger(RuleTrigger newTrigger) updater(clone);
{
trigger = newTrigger; return clone;
trigger.Freeze();
} }
} }
} }

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

@ -13,7 +13,7 @@ using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
{ {
public abstract class RuleAction : Freezable public abstract record RuleAction
{ {
public IEnumerable<ValidationError> Validate() public IEnumerable<ValidationError> Validate()
{ {
@ -41,10 +41,5 @@ namespace Squidex.Domain.Apps.Core.Rules
{ {
yield break; yield break;
} }
public bool DeepEquals(RuleAction action)
{
return SimpleEquals.IsEquals(this, action);
}
} }
} }

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

@ -9,13 +9,8 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
{ {
public abstract class RuleTrigger : Freezable public abstract record RuleTrigger
{ {
public abstract T Accept<T>(IRuleTriggerVisitor<T> visitor); public abstract T Accept<T>(IRuleTriggerVisitor<T> visitor);
public bool DeepEquals(RuleTrigger action)
{
return SimpleEquals.IsEquals(this, action);
}
} }
} }

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTriggerV2.cs

@ -10,9 +10,9 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(AssetChangedTriggerV2))] [TypeName(nameof(AssetChangedTriggerV2))]
public sealed class AssetChangedTriggerV2 : RuleTrigger public sealed record AssetChangedTriggerV2 : RuleTrigger
{ {
public string Condition { get; set; } public string Condition { get; init; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/CommentTrigger.cs

@ -10,9 +10,9 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(CommentTrigger))] [TypeName(nameof(CommentTrigger))]
public sealed class CommentTrigger : RuleTrigger public sealed record CommentTrigger : RuleTrigger
{ {
public string Condition { get; set; } public string Condition { get; init; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {

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

@ -9,10 +9,10 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
public sealed class ContentChangedTriggerSchemaV2 : Freezable public sealed record ContentChangedTriggerSchemaV2
{ {
public DomainId SchemaId { get; set; } public DomainId SchemaId { get; init; }
public string? Condition { get; set; } public string? Condition { get; init; }
} }
} }

21
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs

@ -5,34 +5,21 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(ContentChangedTriggerV2))] [TypeName(nameof(ContentChangedTriggerV2))]
public sealed class ContentChangedTriggerV2 : RuleTrigger public sealed record ContentChangedTriggerV2 : RuleTrigger
{ {
public ReadOnlyCollection<ContentChangedTriggerSchemaV2>? Schemas { get; set; } public ImmutableList<ContentChangedTriggerSchemaV2>? Schemas { get; init; }
public bool HandleAll { get; set; } public bool HandleAll { get; init; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {
return visitor.Visit(this); return visitor.Visit(this);
} }
public override void Freeze()
{
base.Freeze();
if (Schemas != null)
{
foreach (var schema in Schemas)
{
schema.Freeze();
}
}
}
} }
} }

2
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ManualTrigger.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(ManualTrigger))] [TypeName(nameof(ManualTrigger))]
public sealed class ManualTrigger : RuleTrigger public sealed record ManualTrigger : RuleTrigger
{ {
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/SchemaChangedTrigger.cs

@ -10,9 +10,9 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(SchemaChangedTrigger))] [TypeName(nameof(SchemaChangedTrigger))]
public sealed class SchemaChangedTrigger : RuleTrigger public sealed record SchemaChangedTrigger : RuleTrigger
{ {
public string Condition { get; set; } public string Condition { get; init; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {

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

@ -10,11 +10,11 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
[TypeName(nameof(UsageTrigger))] [TypeName(nameof(UsageTrigger))]
public sealed class UsageTrigger : RuleTrigger public sealed record UsageTrigger : RuleTrigger
{ {
public int Limit { get; set; } public int Limit { get; init; }
public int? NumDays { get; set; } public int? NumDays { get; init; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {

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

@ -14,27 +14,22 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class ArrayField : RootField<ArrayFieldProperties>, IArrayField public sealed class ArrayField : RootField<ArrayFieldProperties>, IArrayField
{ {
private FieldCollection<NestedField> fields = FieldCollection<NestedField>.Empty;
public IReadOnlyList<NestedField> Fields public IReadOnlyList<NestedField> Fields
{ {
get => fields.Ordered; get => FieldCollection.Ordered;
} }
public IReadOnlyDictionary<long, NestedField> FieldsById public IReadOnlyDictionary<long, NestedField> FieldsById
{ {
get => fields.ById; get => FieldCollection.ById;
} }
public IReadOnlyDictionary<string, NestedField> FieldsByName public IReadOnlyDictionary<string, NestedField> FieldsByName
{ {
get => fields.ByName; get => FieldCollection.ByName;
} }
public FieldCollection<NestedField> FieldCollection public FieldCollection<NestedField> FieldCollection { get; private set; } = FieldCollection<NestedField>.Empty;
{
get => fields;
}
public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null)
: base(id, name, partitioning, properties, settings) : base(id, name, partitioning, properties, settings)
@ -44,9 +39,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null) public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null)
: this(id, name, partitioning, properties, settings) : this(id, name, partitioning, properties, settings)
{ {
Guard.NotNull(fields, nameof(fields)); FieldCollection = new FieldCollection<NestedField>(fields);
this.fields = new FieldCollection<NestedField>(fields);
} }
[Pure] [Pure]
@ -75,16 +68,16 @@ namespace Squidex.Domain.Apps.Core.Schemas
private ArrayField Updatefields(Func<FieldCollection<NestedField>, FieldCollection<NestedField>> updater) private ArrayField Updatefields(Func<FieldCollection<NestedField>, FieldCollection<NestedField>> updater)
{ {
var newFields = updater(fields); var newFields = updater(FieldCollection);
if (ReferenceEquals(newFields, fields)) if (ReferenceEquals(newFields, FieldCollection))
{ {
return this; return this;
} }
return Clone<ArrayField>(clone => return (ArrayField)Clone(clone =>
{ {
clone.fields = newFields; ((ArrayField)clone).FieldCollection = newFields;
}); });
} }
} }

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

@ -9,12 +9,11 @@ using System;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record ArrayFieldProperties : FieldProperties
public sealed class ArrayFieldProperties : FieldProperties
{ {
public int? MinItems { get; set; } public int? MinItems { get; init; }
public int? MaxItems { get; set; } public int? MaxItems { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

44
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs

@ -5,54 +5,52 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record AssetsFieldProperties : FieldProperties
public sealed class AssetsFieldProperties : FieldProperties
{ {
public AssetPreviewMode PreviewMode { get; set; } public AssetPreviewMode PreviewMode { get; init; }
public LocalizedValue<string[]?> DefaultValues { get; set; } public LocalizedValue<ImmutableList<string>?> DefaultValues { get; init; }
public string[]? DefaultValue { get; set; } public ImmutableList<string>? DefaultValue { get; init; }
public string? FolderId { get; set; } public string? FolderId { get; init; }
public int? MinItems { get; set; } public int? MinItems { get; init; }
public int? MaxItems { get; set; } public int? MaxItems { get; init; }
public int? MinWidth { get; set; } public int? MinWidth { get; init; }
public int? MaxWidth { get; set; } public int? MaxWidth { get; init; }
public int? MinHeight { get; set; } public int? MinHeight { get; init; }
public int? MaxHeight { get; set; } public int? MaxHeight { get; init; }
public int? MinSize { get; set; } public int? MinSize { get; init; }
public int? MaxSize { get; set; } public int? MaxSize { get; init; }
public int? AspectWidth { get; set; } public int? AspectWidth { get; init; }
public int? AspectHeight { get; set; } public int? AspectHeight { get; init; }
public bool MustBeImage { get; set; } public bool MustBeImage { get; init; }
public bool AllowDuplicates { get; set; } public bool AllowDuplicates { get; init; }
public bool ResolveFirst { get; set; } public bool ResolveFirst { get; init; }
public bool ResolveImage public bool ResolveImage
{ {
get => ResolveFirst; init => ResolveFirst = value;
set => ResolveFirst = value;
} }
public ReadOnlyCollection<string>? AllowedExtensions { get; set; } public ImmutableList<string>? AllowedExtensions { get; set; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

11
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs

@ -7,16 +7,15 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record BooleanFieldProperties : FieldProperties
public sealed class BooleanFieldProperties : FieldProperties
{ {
public LocalizedValue<bool?> DefaultValues { get; set; } public LocalizedValue<bool?> DefaultValues { get; init; }
public bool? DefaultValue { get; set; } public bool? DefaultValue { get; init; }
public bool InlineEditable { get; set; } public bool InlineEditable { get; init; }
public BooleanFieldEditor Editor { get; set; } public BooleanFieldEditor Editor { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

15
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs

@ -9,20 +9,19 @@ using NodaTime;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record DateTimeFieldProperties : FieldProperties
public sealed class DateTimeFieldProperties : FieldProperties
{ {
public LocalizedValue<Instant?> DefaultValues { get; set; } public LocalizedValue<Instant?> DefaultValues { get; init; }
public Instant? DefaultValue { get; set; } public Instant? DefaultValue { get; init; }
public Instant? MaxValue { get; set; } public Instant? MaxValue { get; init; }
public Instant? MinValue { get; set; } public Instant? MinValue { get; init; }
public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; init; }
public DateTimeFieldEditor Editor { get; set; } public DateTimeFieldEditor Editor { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

23
backend/src/Squidex.Infrastructure/Cloneable.cs → backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldBase.cs

@ -5,25 +5,24 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using Squidex.Infrastructure;
namespace Squidex.Infrastructure namespace Squidex.Domain.Apps.Core.Schemas
{ {
public abstract class Cloneable public abstract class FieldBase
{ {
protected T Clone<T>(Action<T> updater) where T : Cloneable public long Id { get; }
{
var clone = (T)MemberwiseClone();
updater(clone); public string Name { get; }
clone.OnCloned(); protected FieldBase(long id, string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.GreaterThan(id, 0, nameof(id));
return clone; Id = id;
}
protected virtual void OnCloned() Name = name;
{
} }
} }
} }

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

@ -13,14 +13,14 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class FieldCollection<T> : Cloneable<FieldCollection<T>> where T : IField public sealed class FieldCollection<T> where T : IField
{ {
public static readonly FieldCollection<T> Empty = new FieldCollection<T>(); public static readonly FieldCollection<T> Empty = new FieldCollection<T>();
private static readonly Dictionary<long, T> EmptyById = new Dictionary<long, T>(); private static readonly Dictionary<long, T> EmptyById = new Dictionary<long, T>();
private static readonly Dictionary<string, T> EmptyByString = new Dictionary<string, T>(); private static readonly Dictionary<string, T> EmptyByString = new Dictionary<string, T>();
private T[] fieldsOrdered; private readonly T[] fieldsOrdered;
private Dictionary<long, T>? fieldsById; private Dictionary<long, T>? fieldsById;
private Dictionary<string, T>? fieldsByName; private Dictionary<string, T>? fieldsByName;
@ -81,10 +81,9 @@ namespace Squidex.Domain.Apps.Core.Schemas
fieldsOrdered = fields; fieldsOrdered = fields;
} }
protected override void OnCloned() private FieldCollection(IEnumerable<T> fields)
{ {
fieldsById = null; fieldsOrdered = fields.ToArray();
fieldsByName = null;
} }
[Pure] [Pure]
@ -95,10 +94,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
return this; return this;
} }
return Clone(clone => return new FieldCollection<T>(fieldsOrdered.Where(x => x.Id != fieldId));
{
clone.fieldsOrdered = fieldsOrdered.Where(x => x.Id != fieldId).ToArray();
});
} }
[Pure] [Pure]
@ -116,10 +112,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
return this; return this;
} }
return Clone(clone => return new FieldCollection<T>(fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)));
{
clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToArray();
});
} }
[Pure] [Pure]
@ -137,10 +130,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
throw new ArgumentException($"A field with id {field.Id} already exists.", nameof(field)); throw new ArgumentException($"A field with id {field.Id} already exists.", nameof(field));
} }
return Clone(clone => return new FieldCollection<T>(fieldsOrdered.Union(Enumerable.Repeat(field, 1)));
{
clone.fieldsOrdered = clone.fieldsOrdered.Union(Enumerable.Repeat(field, 1)).ToArray();
});
} }
[Pure] [Pure]
@ -165,10 +155,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
throw new InvalidOperationException($"Field must be of type {typeof(T)}"); throw new InvalidOperationException($"Field must be of type {typeof(T)}");
} }
return Clone(clone => return new FieldCollection<T>(fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x));
{
clone.fieldsOrdered = clone.fieldsOrdered.Select(x => ReferenceEquals(x, field) ? newField : x).ToArray();
});
} }
} }
} }

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

@ -6,25 +6,27 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class FieldNames : ReadOnlyCollection<string> public sealed class FieldNames : ImmutableList<string>
{ {
private static readonly List<string> EmptyNames = new List<string>(); public static readonly FieldNames Empty = new FieldNames(new List<string>());
public static readonly FieldNames Empty = new FieldNames(EmptyNames); public FieldNames()
public FieldNames(params string[] fields)
: base(fields?.ToList() ?? EmptyNames)
{ {
} }
public FieldNames(IList<string> list) public FieldNames(IList<string> list)
: base(list ?? EmptyNames) : base(list)
{
}
public static FieldNames Create(params string[] names)
{ {
return new FieldNames(names.ToList());
} }
public FieldNames Add(string field) public FieldNames Add(string field)

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

@ -5,24 +5,23 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public abstract record FieldProperties : NamedElementPropertiesBase
public abstract class FieldProperties : NamedElementPropertiesBase
{ {
public bool IsRequired { get; set; } public bool IsRequired { get; init; }
public bool IsRequiredOnPublish { get; set; } public bool IsRequiredOnPublish { get; init; }
public bool IsHalfWidth { get; set; } public bool IsHalfWidth { get; init; }
public string? Placeholder { get; set; } public string? Placeholder { get; init; }
public string? EditorUrl { get; set; } public string? EditorUrl { get; init; }
public ReadOnlyCollection<string>? Tags { get; set; } public ImmutableList<string>? Tags { get; init; }
public abstract T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args); public abstract T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args);

19
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs

@ -9,16 +9,15 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record FieldRule
public sealed class FieldRule
{ {
public FieldRuleAction Action { get; } public FieldRuleAction Action { get; }
public string Field { get; } public string Field { get; }
public string? Condition { get; } public string? Condition { get; init; }
public FieldRule(FieldRuleAction action, string field, string? condition) public FieldRule(FieldRuleAction action, string field)
{ {
Guard.Enum(action, nameof(action)); Guard.Enum(action, nameof(action));
Guard.NotNullOrEmpty(field, nameof(field)); Guard.NotNullOrEmpty(field, nameof(field));
@ -26,18 +25,22 @@ namespace Squidex.Domain.Apps.Core.Schemas
Action = action; Action = action;
Field = field; Field = field;
Condition = condition;
} }
public static FieldRule Disable(string field, string? condition = null) public static FieldRule Disable(string field, string? condition = null)
{ {
return new FieldRule(FieldRuleAction.Disable, field, condition); return new FieldRule(FieldRuleAction.Disable, field)
{
Condition = condition
};
} }
public static FieldRule Hide(string field, string? condition = null) public static FieldRule Hide(string field, string? condition = null)
{ {
return new FieldRule(FieldRuleAction.Hide, field, condition); return new FieldRule(FieldRuleAction.Hide, field)
{
Condition = condition
};
} }
} }
} }

18
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs

@ -6,25 +6,27 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class FieldRules : ReadOnlyCollection<FieldRule> public sealed class FieldRules : ImmutableList<FieldRule>
{ {
private static readonly List<FieldRule> EmptyRules = new List<FieldRule>(); public static readonly FieldRules Empty = new FieldRules(new List<FieldRule>());
public static readonly FieldRules Empty = new FieldRules(EmptyRules); public FieldRules()
public FieldRules(params FieldRule[] fields)
: base(fields?.ToList() ?? EmptyRules)
{ {
} }
public FieldRules(IList<FieldRule> list) public FieldRules(IList<FieldRule> list)
: base(list ?? EmptyRules) : base(list)
{
}
public static FieldRules Create(params FieldRule[] rules)
{ {
return new FieldRules(rules.ToArray());
} }
} }
} }

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

@ -7,10 +7,9 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record GeolocationFieldProperties : FieldProperties
public sealed class GeolocationFieldProperties : FieldProperties
{ {
public GeolocationFieldEditor Editor { get; set; } public GeolocationFieldEditor Editor { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

3
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs

@ -7,8 +7,7 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record JsonFieldProperties : FieldProperties
public sealed class JsonFieldProperties : FieldProperties
{ {
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

18
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/LocalizedValue.cs

@ -5,28 +5,20 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Squidex.Infrastructure; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection.Equality;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class LocalizedValue<T> : Dictionary<string, T>, IEquatable<Dictionary<string, T>> public sealed class LocalizedValue<T> : ImmutableDictionary<string, T>
{ {
public override bool Equals(object? obj) public LocalizedValue()
{ {
return Equals(obj as LocalizedValue<T>);
} }
public bool Equals(Dictionary<string, T>? other) public LocalizedValue(IDictionary<string, T> inner)
: base(inner)
{ {
return this.EqualsDictionary(other, EqualityComparer<string>.Default, DeepEqualityComparer<T>.Default);
}
public override int GetHashCode()
{
return this.DictionaryHashCode(EqualityComparer<string>.Default, DeepEqualityComparer<T>.Default);
} }
} }
} }

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

@ -7,10 +7,10 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public abstract class NamedElementPropertiesBase : Freezable public abstract record NamedElementPropertiesBase
{ {
public string? Label { get; set; } public string? Label { get; init; }
public string? Hints { get; set; } public string? Hints { get; init; }
} }
} }

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

@ -5,134 +5,113 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public abstract class NestedField : Cloneable<NestedField>, INestedField public abstract class NestedField : FieldBase, INestedField
{ {
private readonly long fieldId; public bool IsLocked { get; private set; }
private readonly string fieldName;
private bool isDisabled;
private bool isHidden;
private bool isLocked;
public long Id public bool IsHidden { get; private set; }
{
get => fieldId;
}
public string Name
{
get => fieldName;
}
public bool IsLocked
{
get => isLocked;
}
public bool IsHidden
{
get => isHidden;
}
public bool IsDisabled public bool IsDisabled { get; private set; }
{
get => isDisabled;
}
public abstract FieldProperties RawProperties { get; } public abstract FieldProperties RawProperties { get; }
protected NestedField(long id, string name, IFieldSettings? settings = null) protected NestedField(long id, string name, IFieldSettings? settings = null)
: base(id, name)
{ {
Guard.NotNullOrEmpty(name, nameof(name));
Guard.GreaterThan(id, 0, nameof(id));
fieldId = id;
fieldName = name;
if (settings != null) if (settings != null)
{ {
isLocked = settings.IsLocked; IsLocked = settings.IsLocked;
isHidden = settings.IsHidden; IsHidden = settings.IsHidden;
isDisabled = settings.IsDisabled; IsDisabled = settings.IsDisabled;
} }
} }
[Pure] [Pure]
public NestedField Lock() public NestedField Lock()
{ {
if (isLocked) if (IsLocked)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isLocked = true; clone.IsLocked = true;
}); });
} }
[Pure] [Pure]
public NestedField Hide() public NestedField Hide()
{ {
if (isHidden) if (IsHidden)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = true; clone.IsHidden = true;
}); });
} }
[Pure] [Pure]
public NestedField Show() public NestedField Show()
{ {
if (!isHidden) if (!IsHidden)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = false; clone.IsHidden = false;
}); });
} }
[Pure] [Pure]
public NestedField Disable() public NestedField Disable()
{ {
if (isDisabled) if (IsDisabled)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = true; clone.IsDisabled = true;
}); });
} }
[Pure] [Pure]
public NestedField Enable() public NestedField Enable()
{ {
if (!isDisabled) if (!IsDisabled)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = false; clone.IsDisabled = false;
}); });
} }
public abstract T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, TArgs args); public abstract T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, TArgs args);
public abstract NestedField Update(FieldProperties newProperties); public abstract NestedField Update(FieldProperties newProperties);
protected NestedField Clone(Action<NestedField> updater)
{
var clone = (NestedField)MemberwiseClone();
updater(clone);
return clone;
}
} }
} }

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

@ -13,22 +13,17 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
public class NestedField<T> : NestedField, IField<T> where T : FieldProperties, new() public class NestedField<T> : NestedField, IField<T> where T : FieldProperties, new()
{ {
private T properties; public T Properties { get; private set; }
public T Properties
{
get => properties;
}
public override FieldProperties RawProperties public override FieldProperties RawProperties
{ {
get => properties; get => Properties;
} }
public NestedField(long id, string name, T? properties = null, IFieldSettings? settings = null) public NestedField(long id, string name, T? properties = null, IFieldSettings? settings = null)
: base(id, name, settings) : base(id, name, settings)
{ {
SetProperties(properties ?? new T()); Properties = properties ?? new T();
} }
[Pure] [Pure]
@ -36,25 +31,17 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
var typedProperties = ValidateProperties(newProperties); var typedProperties = ValidateProperties(newProperties);
typedProperties.Freeze(); if (Properties.Equals(typedProperties))
if (properties.Equals(typedProperties))
{ {
return this; return this;
} }
return Clone<NestedField<T>>(clone => return Clone(clone =>
{ {
clone.SetProperties(typedProperties); ((NestedField<T>)clone).Properties = typedProperties;
}); });
} }
private void SetProperties(T newProperties)
{
properties = newProperties;
properties.Freeze();
}
private static T ValidateProperties(FieldProperties newProperties) private static T ValidateProperties(FieldProperties newProperties)
{ {
Guard.NotNull(newProperties, nameof(newProperties)); Guard.NotNull(newProperties, nameof(newProperties));
@ -69,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public override TResult Accept<TResult, TArgs>(IFieldVisitor<TResult, TArgs> visitor, TArgs args) public override TResult Accept<TResult, TArgs>(IFieldVisitor<TResult, TArgs> visitor, TArgs args)
{ {
return properties.Accept(visitor, this, args); return Properties.Accept(visitor, this, args);
} }
} }
} }

21
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs

@ -5,28 +5,27 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record NumberFieldProperties : FieldProperties
public sealed class NumberFieldProperties : FieldProperties
{ {
public ReadOnlyCollection<double>? AllowedValues { get; set; } public ImmutableList<double>? AllowedValues { get; init; }
public LocalizedValue<double?> DefaultValues { get; set; } public LocalizedValue<double?> DefaultValues { get; init; }
public double? DefaultValue { get; set; } public double? DefaultValue { get; init; }
public double? MaxValue { get; set; } public double? MaxValue { get; init; }
public double? MinValue { get; set; } public double? MinValue { get; init; }
public bool IsUnique { get; set; } public bool IsUnique { get; init; }
public bool InlineEditable { get; set; } public bool InlineEditable { get; init; }
public NumberFieldEditor Editor { get; set; } public NumberFieldEditor Editor { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

36
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs

@ -5,52 +5,50 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record ReferencesFieldProperties : FieldProperties
public sealed class ReferencesFieldProperties : FieldProperties
{ {
public LocalizedValue<string[]?> DefaultValues { get; set; } public LocalizedValue<ImmutableList<string>?> DefaultValues { get; init; }
public string[]? DefaultValue { get; set; } public ImmutableList<string>? DefaultValue { get; init; }
public int? MinItems { get; set; } public int? MinItems { get; init; }
public int? MaxItems { get; set; } public int? MaxItems { get; init; }
public bool ResolveReference { get; set; } public bool ResolveReference { get; init; }
public bool AllowDuplicates { get; set; } public bool AllowDuplicates { get; init; }
public bool MustBePublished { get; set; } public bool MustBePublished { get; init; }
public ReferencesFieldEditor Editor { get; set; } public ReferencesFieldEditor Editor { get; init; }
public DomainId SchemaId public DomainId SchemaId
{ {
get init
{
return SchemaIds?.FirstOrDefault() ?? default;
}
set
{ {
if (value != default) if (value != default)
{ {
SchemaIds = new ReadOnlyCollection<DomainId>(new List<DomainId> { value }); SchemaIds = ImmutableList.Create(value);
} }
else else
{ {
SchemaIds = null; SchemaIds = null;
} }
} }
get
{
return SchemaIds?.FirstOrDefault() ?? default;
}
} }
public ReadOnlyCollection<DomainId>? SchemaIds { get; set; } public ImmutableList<DomainId>? SchemaIds { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

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

@ -1,147 +1,124 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public abstract class RootField : Cloneable<RootField>, IRootField public abstract class RootField : FieldBase, IRootField
{
private readonly long fieldId;
private readonly string fieldName;
private readonly Partitioning partitioning;
private bool isDisabled;
private bool isHidden;
private bool isLocked;
public long Id
{ {
get => fieldId; public Partitioning Partitioning { get; }
}
public string Name public bool IsLocked { get; private set; }
{
get => fieldName;
}
public bool IsLocked
{
get => isLocked;
}
public bool IsHidden
{
get => isHidden;
}
public bool IsDisabled public bool IsHidden { get; private set; }
{
get => isDisabled;
}
public Partitioning Partitioning public bool IsDisabled { get; private set; }
{
get => partitioning;
}
public abstract FieldProperties RawProperties { get; } public abstract FieldProperties RawProperties { get; }
protected RootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null) protected RootField(long id, string name, Partitioning partitioning, IFieldSettings? settings = null)
: base(id, name)
{ {
Guard.NotNullOrEmpty(name, nameof(name));
Guard.GreaterThan(id, 0, nameof(id));
Guard.NotNull(partitioning, nameof(partitioning)); Guard.NotNull(partitioning, nameof(partitioning));
fieldId = id; Partitioning = partitioning;
fieldName = name;
this.partitioning = partitioning;
if (settings != null) if (settings != null)
{ {
isLocked = settings.IsLocked; IsLocked = settings.IsLocked;
isHidden = settings.IsHidden; IsHidden = settings.IsHidden;
isDisabled = settings.IsDisabled; IsDisabled = settings.IsDisabled;
} }
} }
[Pure] [Pure]
public RootField Lock() public RootField Lock()
{ {
if (isLocked) if (IsLocked)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isLocked = true; clone.IsLocked = true;
}); });
} }
[Pure] [Pure]
public RootField Hide() public RootField Hide()
{ {
if (isHidden) if (IsHidden)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = true; clone.IsHidden = true;
}); });
} }
[Pure] [Pure]
public RootField Show() public RootField Show()
{ {
if (!isHidden) if (!IsHidden)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isHidden = false; clone.IsHidden = false;
}); });
} }
[Pure] [Pure]
public RootField Disable() public RootField Disable()
{ {
if (isDisabled) if (IsDisabled)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = true; clone.IsDisabled = true;
}); });
} }
[Pure] [Pure]
public RootField Enable() public RootField Enable()
{ {
if (!isDisabled) if (!IsDisabled)
{ {
return this; return this;
} }
return Clone(clone => return Clone(clone =>
{ {
clone.isDisabled = false; clone.IsDisabled = false;
}); });
} }
public abstract T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, TArgs args); public abstract T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, TArgs args);
public abstract RootField Update(FieldProperties newProperties); public abstract RootField Update(FieldProperties newProperties);
protected RootField Clone(Action<RootField> updater)
{
var clone = (RootField)MemberwiseClone();
updater(clone);
return clone;
}
} }
} }

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

@ -13,22 +13,17 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
public class RootField<T> : RootField, IField<T> where T : FieldProperties, new() public class RootField<T> : RootField, IField<T> where T : FieldProperties, new()
{ {
private T properties; public T Properties { get; private set; }
public T Properties
{
get => properties;
}
public override FieldProperties RawProperties public override FieldProperties RawProperties
{ {
get => properties; get => Properties;
} }
public RootField(long id, string name, Partitioning partitioning, T? properties = null, IFieldSettings? settings = null) public RootField(long id, string name, Partitioning partitioning, T? properties = null, IFieldSettings? settings = null)
: base(id, name, partitioning, settings) : base(id, name, partitioning, settings)
{ {
SetProperties(properties ?? new T()); Properties = properties ?? new T();
} }
[Pure] [Pure]
@ -36,23 +31,17 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
var typedProperties = ValidateProperties(newProperties); var typedProperties = ValidateProperties(newProperties);
if (properties.Equals(typedProperties)) if (Properties.Equals(typedProperties))
{ {
return this; return this;
} }
return Clone<RootField<T>>(clone => return Clone(clone =>
{ {
clone.SetProperties(typedProperties); ((RootField<T>)clone).Properties = typedProperties;
}); });
} }
private void SetProperties(T newProperties)
{
properties = newProperties;
properties.Freeze();
}
private static T ValidateProperties(FieldProperties newProperties) private static T ValidateProperties(FieldProperties newProperties)
{ {
Guard.NotNull(newProperties, nameof(newProperties)); Guard.NotNull(newProperties, nameof(newProperties));
@ -67,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public override TResult Accept<TResult, TArgs>(IFieldVisitor<TResult, TArgs> visitor, TArgs args) public override TResult Accept<TResult, TArgs>(IFieldVisitor<TResult, TArgs> visitor, TArgs args)
{ {
return properties.Accept(visitor, this, args); return Properties.Accept(visitor, this, args);
} }
} }
} }

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

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -13,7 +13,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public sealed class Schema : Cloneable<Schema> public sealed class Schema
{ {
private static readonly Dictionary<string, string> EmptyPreviewUrls = new Dictionary<string, string>(); private static readonly Dictionary<string, string> EmptyPreviewUrls = new Dictionary<string, string>();
private readonly string name; private readonly string name;
@ -105,7 +105,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.name = name; this.name = name;
this.properties = properties ?? new SchemaProperties(); this.properties = properties ?? new SchemaProperties();
this.properties.Freeze();
this.isSingleton = isSingleton; this.isSingleton = isSingleton;
} }
@ -133,7 +132,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
return Clone(clone => return Clone(clone =>
{ {
clone.properties = newProperties; clone.properties = newProperties;
clone.Properties.Freeze();
}); });
} }
@ -150,7 +148,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
return Clone(clone => return Clone(clone =>
{ {
clone.scripts = newScripts; clone.scripts = newScripts;
clone.scripts.Freeze();
}); });
} }
@ -203,7 +200,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
rules ??= FieldRules.Empty; rules ??= FieldRules.Empty;
if (fieldRules.SetEquals(rules)) if (fieldRules.Equals(rules))
{ {
return this; return this;
} }
@ -326,5 +323,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
clone.fields = newFields; clone.fields = newFields;
}); });
} }
private Schema Clone(Action<Schema> updater)
{
var clone = (Schema)MemberwiseClone();
updater(clone);
return clone;
}
} }
} }

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

@ -1,25 +1,24 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record SchemaProperties : NamedElementPropertiesBase
public sealed class SchemaProperties : NamedElementPropertiesBase
{ {
public ReadOnlyCollection<string>? Tags { get; set; } public ImmutableList<string>? Tags { get; init; }
public string? ContentsSidebarUrl { get; set; } public string? ContentsSidebarUrl { get; init; }
public string? ContentSidebarUrl { get; set; } public string? ContentSidebarUrl { get; init; }
public string? ContentEditorUrl { get; set; } public string? ContentEditorUrl { get; init; }
public bool ValidateOnPublish { get; set; } public bool ValidateOnPublish { get; init; }
} }
} }

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

@ -7,24 +7,18 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators =true)] public sealed record SchemaScripts
public sealed class SchemaScripts : Freezable
{ {
public static readonly SchemaScripts Empty = new SchemaScripts(); public static readonly SchemaScripts Empty = new SchemaScripts();
static SchemaScripts() public string Change { get; init; }
{
Empty.Freeze();
}
public string Change { get; set; }
public string Create { get; set; } public string Create { get; init; }
public string Update { get; set; } public string Update { get; init; }
public string Delete { get; set; } public string Delete { get; init; }
public string Query { get; set; } public string Query { get; init; }
} }
} }

37
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -5,44 +5,43 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record StringFieldProperties : FieldProperties
public sealed class StringFieldProperties : FieldProperties
{ {
public ReadOnlyCollection<string>? AllowedValues { get; set; } public ImmutableList<string>? AllowedValues { get; init; }
public LocalizedValue<string?> DefaultValues { get; set; } public LocalizedValue<string?> DefaultValues { get; init; }
public string? DefaultValue { get; set; } public string? DefaultValue { get; init; }
public string? Pattern { get; set; } public string? Pattern { get; init; }
public string? PatternMessage { get; set; } public string? PatternMessage { get; init; }
public string? FolderId { get; set; } public string? FolderId { get; init; }
public int? MinLength { get; set; } public int? MinLength { get; init; }
public int? MaxLength { get; set; } public int? MaxLength { get; init; }
public int? MinCharacters { get; set; } public int? MinCharacters { get; init; }
public int? MaxCharacters { get; set; } public int? MaxCharacters { get; init; }
public int? MinWords { get; set; } public int? MinWords { get; init; }
public int? MaxWords { get; set; } public int? MaxWords { get; init; }
public bool IsUnique { get; set; } public bool IsUnique { get; init; }
public bool InlineEditable { get; set; } public bool InlineEditable { get; init; }
public StringContentType ContentType { get; set; } public StringContentType ContentType { get; init; }
public StringFieldEditor Editor { get; set; } public StringFieldEditor Editor { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

19
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs

@ -5,26 +5,25 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record TagsFieldProperties : FieldProperties
public sealed class TagsFieldProperties : FieldProperties
{ {
public ReadOnlyCollection<string>? AllowedValues { get; set; } public ImmutableList<string>? AllowedValues { get; init; }
public LocalizedValue<string[]?> DefaultValues { get; set; } public LocalizedValue<ImmutableList<string>?> DefaultValues { get; init; }
public string[]? DefaultValue { get; set; } public ImmutableList<string>? DefaultValue { get; init; }
public int? MinItems { get; set; } public int? MinItems { get; init; }
public int? MaxItems { get; set; } public int? MaxItems { get; init; }
public TagsFieldEditor Editor { get; set; } public TagsFieldEditor Editor { get; init; }
public TagsFieldNormalization Normalization { get; set; } public TagsFieldNormalization Normalization { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

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

@ -7,10 +7,9 @@
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
[Equals(DoNotAddEqualityOperators = true)] public sealed record UIFieldProperties : FieldProperties
public sealed class UIFieldProperties : FieldProperties
{ {
public UIFieldEditor Editor { get; set; } public UIFieldEditor Editor { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {

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

@ -9,12 +9,6 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Fody" Version="6.5.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SquidexFreezable.Fody" Version="2.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="5.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

3
backend/src/Squidex.Domain.Apps.Core.Operations/DefaultValues/DefaultValueFactory.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
@ -132,7 +133,7 @@ namespace Squidex.Domain.Apps.Core.DefaultValues
return value; return value;
} }
private static IJsonValue Array(string[]? values) private static IJsonValue Array(IEnumerable<string>? values)
{ {
if (values != null) if (values != null)
{ {

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

@ -65,17 +65,17 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
yield return @event; yield return @event;
} }
if (!source.FieldsInLists.SequenceEqual(target.FieldsInLists)) if (!source.FieldsInLists.Equals(target.FieldsInLists))
{ {
yield return new SchemaUIFieldsConfigured { FieldsInLists = target.FieldsInLists }; yield return new SchemaUIFieldsConfigured { FieldsInLists = target.FieldsInLists };
} }
if (!source.FieldsInReferences.SequenceEqual(target.FieldsInReferences)) if (!source.FieldsInReferences.Equals(target.FieldsInReferences))
{ {
yield return new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }; yield return new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences };
} }
if (!source.FieldRules.SetEquals(target.FieldRules)) if (!source.FieldRules.Equals(target.FieldRules))
{ {
yield return new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }; yield return new SchemaFieldRulesConfigured { FieldRules = target.FieldRules };
} }
@ -196,8 +196,8 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if (sourceIds.Count > 1) if (sourceIds.Count > 1)
{ {
var sourceNames = sourceIds.Select(x => x.Name).ToList(); var sourceNames = sourceIds.Select(x => x.Name).ToHashSet();
var targetNames = target.Ordered.Select(x => x.Name).ToList(); var targetNames = target.Ordered.Select(x => x.Name).ToHashSet();
if (sourceNames.SetEquals(targetNames) && !sourceNames.SequenceEqual(targetNames)) if (sourceNames.SetEquals(targetNames) && !sourceNames.SequenceEqual(targetNames))
{ {

10
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -310,8 +310,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
var events = new List<AppEvent> var events = new List<AppEvent>
{ {
CreateInitalEvent(command.Name), CreateInitalEvent(command.Name)
CreateInitialLanguage()
}; };
if (command.Actor.IsUser) if (command.Actor.IsUser)
@ -450,11 +449,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return new AppCreated { Name = name }; return new AppCreated { Name = name };
} }
private static AppLanguageAdded CreateInitialLanguage()
{
return new AppLanguageAdded { Language = Language.EN };
}
private static AppContributorAssigned CreateInitialOwner(RefToken actor) private static AppContributorAssigned CreateInitialOwner(RefToken actor)
{ {
return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner }; return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner };

10
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs

@ -19,14 +19,20 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public AssetFieldBuilder MustBeImage() public AssetFieldBuilder MustBeImage()
{ {
Properties<AssetsFieldProperties>().MustBeImage = true; Properties<AssetsFieldProperties>(p => p with
{
MustBeImage = true
});
return this; return this;
} }
public AssetFieldBuilder RequireSingle() public AssetFieldBuilder RequireSingle()
{ {
Properties<AssetsFieldProperties>().MaxItems = 2; Properties<AssetsFieldProperties>(p => p with
{
MaxItems = 1
});
return this; return this;
} }

6
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs

@ -19,7 +19,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public BooleanFieldBuilder AsToggle() public BooleanFieldBuilder AsToggle()
{ {
Properties<BooleanFieldProperties>().Editor = BooleanFieldEditor.Toggle; Properties<BooleanFieldProperties>(p => p with
{
Editor = BooleanFieldEditor.Toggle,
EditorUrl = null
});
return this; return this;
} }

6
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs

@ -19,7 +19,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public DateTimeFieldBuilder AsDateTime() public DateTimeFieldBuilder AsDateTime()
{ {
Properties<DateTimeFieldProperties>().Editor = DateTimeFieldEditor.DateTime; Properties<DateTimeFieldProperties>(p => p with
{
Editor = DateTimeFieldEditor.DateTime,
EditorUrl = null
});
return this; return this;
} }

17
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
@ -16,11 +17,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
private readonly UpsertSchemaField field; private readonly UpsertSchemaField field;
private readonly CreateSchema schema; private readonly CreateSchema schema;
protected T Properties<T>() where T : FieldProperties
{
return (T)field.Properties;
}
protected FieldBuilder(UpsertSchemaField field, CreateSchema schema) protected FieldBuilder(UpsertSchemaField field, CreateSchema schema)
{ {
this.field = field; this.field = field;
@ -29,14 +25,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public FieldBuilder Label(string? label) public FieldBuilder Label(string? label)
{ {
field.Properties.Label = label; field.Properties = field.Properties with { Label = label };
return this; return this;
} }
public FieldBuilder Hints(string? hints) public FieldBuilder Hints(string? hints)
{ {
field.Properties.Hints = hints; field.Properties = field.Properties with { Hints = hints };
return this; return this;
} }
@ -57,11 +53,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public FieldBuilder Required() public FieldBuilder Required()
{ {
field.Properties.IsRequired = true; field.Properties = field.Properties with { IsRequired = true };
return this; return this;
} }
protected void Properties<T>(Func<T, T> updater) where T : FieldProperties
{
field.Properties = updater((T)field.Properties);
}
public FieldBuilder ShowInList() public FieldBuilder ShowInList()
{ {
schema.FieldsInLists ??= new FieldNames(); schema.FieldsInLists ??= new FieldNames();

6
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/ReferencesFieldBuilder.cs

@ -8,6 +8,7 @@
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{ {
@ -20,7 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public ReferencesFieldBuilder WithSchemaId(DomainId id) public ReferencesFieldBuilder WithSchemaId(DomainId id)
{ {
Properties<ReferencesFieldProperties>().SchemaId = id; Properties<ReferencesFieldProperties>(p => p with
{
SchemaIds = ImmutableList.Create(id)
});
return this; return this;
} }

10
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs

@ -34,8 +34,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public SchemaBuilder WithLabel(string? label) public SchemaBuilder WithLabel(string? label)
{ {
command.Properties ??= new SchemaProperties(); if (command.Properties == null)
command.Properties.Label = label; {
command.Properties = new SchemaProperties { Label = label };
}
else
{
command.Properties = command.Properties with { Label = label };
}
return this; return this;
} }

39
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs

@ -20,45 +20,66 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public StringFieldBuilder AsTextArea() public StringFieldBuilder AsTextArea()
{ {
Properties<StringFieldProperties>().Editor = StringFieldEditor.TextArea; Properties<StringFieldProperties>(p => p with
{
EditorUrl = null,
Editor = StringFieldEditor.TextArea
});
return this; return this;
} }
public StringFieldBuilder AsRichText() public StringFieldBuilder AsRichText()
{ {
Properties<StringFieldProperties>().Editor = StringFieldEditor.RichText; Properties<StringFieldProperties>(p => p with
{
EditorUrl = null,
Editor = StringFieldEditor.RichText
});
return this; return this;
} }
public StringFieldBuilder AsDropDown(params string[] values) public StringFieldBuilder AsDropDown(params string[] values)
{ {
Properties<StringFieldProperties>().AllowedValues = ReadOnlyCollection.Create(values); Properties<StringFieldProperties>(p => p with
Properties<StringFieldProperties>().Editor = StringFieldEditor.Dropdown; {
AllowedValues = ImmutableList.Create(values),
EditorUrl = null,
Editor = StringFieldEditor.Dropdown
});
return this; return this;
} }
public StringFieldBuilder Unique() public StringFieldBuilder Unique()
{ {
Properties<StringFieldProperties>().IsUnique = true; Properties<StringFieldProperties>(p => p with
{
IsUnique = true
});
return this; return this;
} }
public StringFieldBuilder Pattern(string pattern, string? message = null) public StringFieldBuilder Pattern(string pattern, string? message = null)
{ {
Properties<StringFieldProperties>().Pattern = pattern; Properties<StringFieldProperties>(p => p with
Properties<StringFieldProperties>().PatternMessage = message; {
Pattern = pattern,
PatternMessage = message
});
return this; return this;
} }
public StringFieldBuilder Length(int maxLength, int minLength = 0) public StringFieldBuilder Length(int maxLength, int minLength = 0)
{ {
Properties<StringFieldProperties>().MaxLength = maxLength; Properties<StringFieldProperties>(p => p with
Properties<StringFieldProperties>().MinLength = minLength; {
MaxLength = maxLength,
MinLength = minLength
});
return this; return this;
} }

7
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{ {
@ -20,7 +20,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public TagsFieldBuilder WithAllowedValues(params string[] values) public TagsFieldBuilder WithAllowedValues(params string[] values)
{ {
Properties<TagsFieldProperties>().AllowedValues = new ReadOnlyCollection<string>(values); Properties<TagsFieldProperties>(p => p with
{
AllowedValues = ImmutableList.Create(values)
});
return this; return this;
} }

5
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs

@ -19,7 +19,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
public FieldRule ToFieldRule() public FieldRule ToFieldRule()
{ {
return new FieldRule(Action, Field, Condition); return new FieldRule(Action, Field)
{
Condition = Condition
};
} }
} }
} }

42
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/IUpsertCommand.cs

@ -82,7 +82,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
var partitioning = Partitioning.FromString(eventField.Partitioning); var partitioning = Partitioning.FromString(eventField.Partitioning);
var field = eventField.Properties.CreateRootField(totalFields, eventField.Name, partitioning); var field =
eventField.Properties.CreateRootField(
totalFields,
eventField.Name, partitioning,
eventField);
if (field is ArrayField arrayField && eventField.Nested?.Length > 0) if (field is ArrayField arrayField && eventField.Nested?.Length > 0)
{ {
@ -90,22 +94,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
totalFields++; totalFields++;
var nestedField = nestedEventField.Properties.CreateNestedField(totalFields, nestedEventField.Name); var nestedField =
nestedEventField.Properties.CreateNestedField(
if (nestedEventField.IsHidden) totalFields,
{ nestedEventField.Name,
nestedField = nestedField.Hide(); nestedEventField);
}
if (nestedEventField.IsDisabled)
{
nestedField = nestedField.Disable();
}
if (nestedEventField.IsLocked)
{
nestedField = nestedField.Lock();
}
arrayField = arrayField.AddField(nestedField); arrayField = arrayField.AddField(nestedField);
} }
@ -113,21 +106,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
field = arrayField; field = arrayField;
} }
if (eventField.IsHidden)
{
field = field.Hide();
}
if (eventField.IsDisabled)
{
field = field.Disable();
}
if (eventField.IsLocked)
{
field = field.Lock();
}
schema = schema.AddField(field); schema = schema.AddField(field);
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs

@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
public abstract class UpsertSchemaFieldBase public abstract class UpsertSchemaFieldBase : IFieldSettings
{ {
public string Name { get; set; } public string Name { get; set; }

2
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCreatedFieldBase.cs

@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Events.Schemas namespace Squidex.Domain.Apps.Events.Schemas
{ {
public abstract class SchemaCreatedFieldBase public abstract class SchemaCreatedFieldBase : IFieldSettings
{ {
public string Name { get; set; } public string Name { get; set; }

19
backend/src/Squidex.Infrastructure/Cloneable{T}.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure
{
public abstract class Cloneable<T> : Cloneable where T : Cloneable
{
protected T Clone(Action<T> updater)
{
return base.Clone(updater);
}
}
}

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

@ -7,12 +7,87 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {
public static class CollectionExtensions public static class CollectionExtensions
{ {
public static bool TryAdd<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull
{
result = null;
if (!source.ContainsKey(key))
{
var clone = new Dictionary<TKey, TValue>(source)
{
[key] = value
};
result = clone;
return true;
}
return false;
}
public static bool TrySet<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull
{
result = null;
if (!source.TryGetValue(key, out var found) || !Equals(found, value))
{
var clone = new Dictionary<TKey, TValue>(source)
{
[key] = value
};
result = clone;
return true;
}
return false;
}
public static bool TryUpdate<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, TValue value, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull
{
result = null;
if (source.TryGetValue(key, out var found) && !Equals(found, value))
{
var clone = new Dictionary<TKey, TValue>(source)
{
[key] = value
};
result = clone;
return true;
}
return false;
}
public static bool TryRemove<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> source, TKey key, [MaybeNullWhen(false)] out Dictionary<TKey, TValue> result) where TKey : notnull
{
result = null;
if (source.ContainsKey(key))
{
var clone = new Dictionary<TKey, TValue>(source);
result = clone;
result.Remove(key);
return true;
}
return false;
}
public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other) public static bool SetEquals<T>(this IReadOnlyCollection<T> source, IReadOnlyCollection<T> other)
{ {
return source.Count == other.Count && source.Intersect(other).Count() == other.Count; return source.Count == other.Count && source.Intersect(other).Count() == other.Count;
@ -152,27 +227,6 @@ namespace Squidex.Infrastructure
return hashCode; return hashCode;
} }
public static int OrderedHashCode<T>(this IEnumerable<T> collection) where T : notnull
{
return collection.OrderedHashCode(EqualityComparer<T>.Default);
}
public static int OrderedHashCode<T>(this IEnumerable<T> collection, IEqualityComparer<T> comparer) where T : notnull
{
Guard.NotNull(comparer, nameof(comparer));
var hashCodes = collection.Where(x => !Equals(x, null)).Select(x => x.GetHashCode()).OrderBy(x => x).ToArray();
var hashCode = 17;
foreach (var code in hashCodes)
{
hashCode = (hashCode * 23) + code;
}
return hashCode;
}
public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull public static int DictionaryHashCode<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull
{ {
return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default); return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default);
@ -222,6 +276,39 @@ namespace Squidex.Infrastructure
return !dictionary.Except(other, comparer).Any(); return !dictionary.Except(other, comparer).Any();
} }
public static bool EqualsList<T>(this IReadOnlyList<T> list, IReadOnlyList<T>? other)
{
return EqualsList(list, other, EqualityComparer<T>.Default);
}
public static bool EqualsList<T>(this IReadOnlyList<T> list, IReadOnlyList<T>? other, IEqualityComparer<T> comparer)
{
if (other == null)
{
return false;
}
if (ReferenceEquals(list, other))
{
return true;
}
if (list.Count != other.Count)
{
return false;
}
for (var i = 0; i < list.Count; i++)
{
if (!comparer.Equals(list[i], other[i]))
{
return false;
}
}
return true;
}
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) where TKey : notnull
{ {
return dictionary.ToDictionary(x => x.Key, x => x.Value); return dictionary.ToDictionary(x => x.Key, x => x.Value);

45
backend/src/Squidex.Infrastructure/Collections/ImmutableDictionary.cs

@ -13,9 +13,50 @@ namespace Squidex.Infrastructure.Collections
{ {
public static class ImmutableDictionary public static class ImmutableDictionary
{ {
public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TKey, TValue>(this IEnumerable<TValue> source, Func<TValue, TKey> keyExtractor) where TKey : notnull private static class Empties<TKey, TValue> where TKey : notnull
{ {
return new ImmutableDictionary<TKey, TValue>(source.ToDictionary(keyExtractor)); #pragma warning disable SA1401 // Fields should be private
public static ImmutableDictionary<TKey, TValue> Instance = new ImmutableDictionary<TKey, TValue>();
#pragma warning restore SA1401 // Fields should be private
}
public static ImmutableDictionary<TKey, TValue> Empty<TKey, TValue>() where TKey : notnull
{
return Empties<TKey, TValue>.Instance;
}
public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TKey, TValue>(this Dictionary<TKey, TValue> source) where TKey : notnull
{
if (source.Count == 0)
{
return Empty<TKey, TValue>();
}
return new ImmutableDictionary<TKey, TValue>(source);
}
public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TKey, TValue>(this IEnumerable<TValue> source, Func<TValue, TKey> keySelector) where TKey : notnull
{
var inner = source.ToDictionary(keySelector);
if (inner.Count == 0)
{
return Empty<TKey, TValue>();
}
return new ImmutableDictionary<TKey, TValue>(inner);
}
public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TSource, TKey, TValue>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TValue> elementSelector) where TKey : notnull
{
var inner = source.ToDictionary(keySelector, elementSelector);
if (inner.Count == 0)
{
return Empty<TKey, TValue>();
}
return new ImmutableDictionary<TKey, TValue>(inner);
} }
} }
} }

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

Loading…
Cancel
Save