Browse Source

Merge pull request #152 from Squidex/refactoring-simple-model

Refactoring simple model
pull/158/merge
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
24d38a3269
  1. 41
      Squidex.sln
  2. 5
      Squidex.sln.DotSettings
  3. 59
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  4. 17
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs
  5. 37
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  6. 17
      src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs
  7. 30
      src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  8. 18
      src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs
  9. 29
      src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs
  10. 43
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs
  11. 43
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs
  12. 39
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs
  13. 40
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs
  14. 45
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs
  15. 66
      src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs
  16. 178
      src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  17. 30
      src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs
  18. 89
      src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
  19. 54
      src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  20. 51
      src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs
  21. 45
      src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  22. 17
      src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  23. 33
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs
  24. 63
      src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs
  25. 14
      src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs
  26. 19
      src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs
  27. 73
      src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs
  28. 50
      src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
  29. 27
      src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs
  30. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs
  31. 25
      src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  32. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs
  33. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs
  34. 25
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  35. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs
  36. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs
  37. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs
  38. 32
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
  39. 95
      src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs
  40. 21
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  41. 116
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
  42. 54
      src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs
  43. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs
  44. 15
      src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs
  45. 23
      src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
  46. 31
      src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
  47. 31
      src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
  48. 36
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
  49. 99
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  50. 36
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs
  51. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs
  52. 21
      src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
  53. 17
      src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs
  54. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs
  55. 18
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs
  56. 31
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  57. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs
  58. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  59. 120
      src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  60. 14
      src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  61. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs
  62. 20
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs
  63. 35
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  64. 28
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs
  65. 25
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
  66. 20
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  67. 25
      src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs
  68. 202
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs
  69. 78
      src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs
  70. 23
      src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs
  71. 87
      src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs
  72. 75
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs
  73. 58
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  74. 63
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs
  75. 61
      src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs
  76. 77
      src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs
  77. 57
      src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs
  78. 72
      src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs
  79. 163
      src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  80. 132
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  81. 68
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
  82. 137
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  83. 59
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs
  84. 146
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  85. 21
      src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
  86. 178
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  87. 50
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
  88. 27
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  89. 28
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  90. 43
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs
  91. 142
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  92. 58
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs
  93. 113
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  94. 59
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  95. 45
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs
  96. 30
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  97. 48
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs
  98. 54
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs
  99. 18
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs
  100. 48
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs

41
Squidex.sln

@ -8,8 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastru
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "domain", "domain", "{4C6B06C2-6D77-4E0E-AE32-D7050236433A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core", "src\Squidex.Domain.Apps.Core\Squidex.Domain.Apps.Core.csproj", "{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure", "src\Squidex.Infrastructure\Squidex.Infrastructure.csproj", "{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Events", "src\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj", "{25F66C64-058A-4D44-BC0C-F12A054F9A91}"
@ -65,6 +63,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
stylecop.json = stylecop.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Model", "src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj", "{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Operations", "src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj", "{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -83,14 +85,6 @@ Global
{61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|Any CPU.Build.0 = Release|Any CPU
{61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|x64.ActiveCfg = Release|Any CPU
{61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|x86.ActiveCfg = Release|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|x64.ActiveCfg = Debug|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|x86.ActiveCfg = Debug|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|Any CPU.Build.0 = Release|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|x64.ActiveCfg = Release|Any CPU
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|x86.ActiveCfg = Release|Any CPU
{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -299,12 +293,35 @@ Global
{7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x64.Build.0 = Release|Any CPU
{7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x86.ActiveCfg = Release|Any CPU
{7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x86.Build.0 = Release|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x64.ActiveCfg = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x64.Build.0 = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x86.ActiveCfg = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x86.Build.0 = Debug|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|Any CPU.Build.0 = Release|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x64.ActiveCfg = Release|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x64.Build.0 = Release|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x86.ActiveCfg = Release|Any CPU
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x86.Build.0 = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x64.ActiveCfg = Debug|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x64.Build.0 = Debug|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x86.ActiveCfg = Debug|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x86.Build.0 = Debug|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|Any CPU.Build.0 = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x64.ActiveCfg = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x64.Build.0 = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.ActiveCfg = Release|Any CPU
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0} = {C9809D59-6665-471E-AD87-5AC624C65892}
{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{25F66C64-058A-4D44-BC0C-F12A054F9A91} = {C9809D59-6665-471E-AD87-5AC624C65892}
{A85201C6-6AF8-4B63-8365-08F741050438} = {C9809D59-6665-471E-AD87-5AC624C65892}
@ -328,6 +345,8 @@ Global
{42184546-E3CB-4D4F-9495-43979B9C63B9} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA}
{EF75E488-1324-4E18-A1BD-D3A05AE67B1F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892}
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}

5
Squidex.sln.DotSettings

@ -13,6 +13,11 @@
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ets/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002E_002A/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Header/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Header"&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Namespaces/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Namespaces"&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;EmbraceInRegion&gt;False&lt;/EmbraceInRegion&gt;&lt;RegionName&gt;&lt;/RegionName&gt;&lt;/CSOptimizeUsings&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Typescript/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Typescript"&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;CorrectVariableKindsDescriptor&gt;True&lt;/CorrectVariableKindsDescriptor&gt;&lt;VariablesToInnerScopesDescriptor&gt;True&lt;/VariablesToInnerScopesDescriptor&gt;&lt;StringToTemplatesDescriptor&gt;True&lt;/StringToTemplatesDescriptor&gt;&lt;RemoveRedundantQualifiersTs&gt;True&lt;/RemoveRedundantQualifiersTs&gt;&lt;OptimizeImportsTs&gt;True&lt;/OptimizeImportsTs&gt;&lt;/Profile&gt;</s:String>

59
src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -0,0 +1,59 @@
// ==========================================================================
// AppClient.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppClient
{
private readonly string secret;
private string name;
private AppClientPermission permission;
public string Name
{
get { return name; }
}
public string Secret
{
get { return secret; }
}
public AppClientPermission Permission
{
get { return permission; }
}
public AppClient(string name, string secret, AppClientPermission permission)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(secret, nameof(secret));
Guard.Enum(permission, nameof(permission));
this.name = name;
this.secret = secret;
this.permission = permission;
}
public void Update(AppClientPermission newPermission)
{
Guard.Enum(newPermission, nameof(newPermission));
permission = newPermission;
}
public void Rename(string newName)
{
Guard.NotNullOrEmpty(newName, nameof(newName));
name = newName;
}
}
}

17
src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs

@ -0,0 +1,17 @@
// ==========================================================================
// AppClientPermission.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Apps
{
public enum AppClientPermission
{
Developer,
Editor,
Reader
}
}

37
src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -0,0 +1,37 @@
// ==========================================================================
// AppClients.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppClients : DictionaryBase<string, AppClient>
{
public void Add(string id, AppClient client)
{
Guard.NotNullOrEmpty(id, nameof(id));
Guard.NotNull(client, nameof(client));
Inner.Add(id, client);
}
public void Add(string id, string secret)
{
Guard.NotNullOrEmpty(id, nameof(id));
Inner.Add(id, new AppClient(id, secret, AppClientPermission.Editor));
}
public void Revoke(string id)
{
Guard.NotNullOrEmpty(id, nameof(id));
Inner.Remove(id);
}
}
}

17
src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs

@ -0,0 +1,17 @@
// ==========================================================================
// AppContributorPermission.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Apps
{
public enum AppContributorPermission
{
Owner,
Developer,
Editor
}
}

30
src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs

@ -0,0 +1,30 @@
// ==========================================================================
// AppContributors.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppContributors : DictionaryBase<string, AppContributorPermission>
{
public void Assign(string contributorId, AppContributorPermission permission)
{
Guard.NotNullOrEmpty(contributorId, nameof(contributorId));
Guard.Enum(permission, nameof(permission));
Inner[contributorId] = permission;
}
public void Remove(string contributorId)
{
Guard.NotNullOrEmpty(contributorId, nameof(contributorId));
Inner.Remove(contributorId);
}
}
}

18
src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs

@ -0,0 +1,18 @@
// ==========================================================================
// AppPermission.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Apps
{
public enum AppPermission
{
Owner,
Developer,
Editor,
Reader
}
}

29
src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs

@ -0,0 +1,29 @@
// ==========================================================================
// AppPlan.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPlan
{
public RefToken Owner { get; }
public string PlanId { get; }
public AppPlan(RefToken owner, string planId)
{
Guard.NotNull(owner, nameof(owner));
Guard.NotNullOrEmpty(planId, nameof(planId));
Owner = owner;
PlanId = planId;
}
}
}

43
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs

@ -0,0 +1,43 @@
// ==========================================================================
// AppClientsConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class AppClientsConverter : JsonClassConverter<AppClients>
{
protected override void WriteValue(JsonWriter writer, AppClients value, JsonSerializer serializer)
{
var json = new Dictionary<string, JsonAppClient>(value.Count);
foreach (var client in value)
{
json.Add(client.Key, new JsonAppClient(client.Value));
}
serializer.Serialize(writer, json);
}
protected override AppClients ReadValue(JsonReader reader, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, JsonAppClient>>(reader);
var clients = new AppClients();
foreach (var client in json)
{
clients.Add(client.Key, client.Value.ToClient());
}
return clients;
}
}
}

43
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs

@ -0,0 +1,43 @@
// ==========================================================================
// AppContributorsConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class AppContributorsConverter : JsonClassConverter<AppContributors>
{
protected override void WriteValue(JsonWriter writer, AppContributors value, JsonSerializer serializer)
{
var json = new Dictionary<string, AppContributorPermission>(value.Count);
foreach (var contributor in value)
{
json.Add(contributor.Key, contributor.Value);
}
serializer.Serialize(writer, json);
}
protected override AppContributors ReadValue(JsonReader reader, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, AppContributorPermission>>(reader);
var contributors = new AppContributors();
foreach (var contributor in json)
{
contributors.Assign(contributor.Key, contributor.Value);
}
return contributors;
}
}
}

39
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs

@ -0,0 +1,39 @@
// ==========================================================================
// JsonAppClient.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public class JsonAppClient
{
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public string Secret { get; set; }
[JsonProperty]
public AppClientPermission Permission { get; set; }
public JsonAppClient()
{
}
public JsonAppClient(AppClient client)
{
SimpleMapper.Map(client, this);
}
public AppClient ToClient()
{
return new AppClient(Name, Secret, Permission);
}
}
}

40
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs

@ -0,0 +1,40 @@
// ==========================================================================
// JsonLanguageConfig.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public class JsonLanguageConfig
{
[JsonProperty]
public Language[] Fallback { get; set; }
[JsonProperty]
public bool IsOptional { get; set; }
public JsonLanguageConfig()
{
}
public JsonLanguageConfig(LanguageConfig config)
{
SimpleMapper.Map(config, this);
Fallback = config.LanguageFallbacks.ToArray();
}
public LanguageConfig ToConfig(string language)
{
return new LanguageConfig(language, IsOptional, Fallback);
}
}
}

45
src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs

@ -0,0 +1,45 @@
// ==========================================================================
// AppClientsConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class LanguagesConfigConverter : JsonClassConverter<LanguagesConfig>
{
protected override void WriteValue(JsonWriter writer, LanguagesConfig value, JsonSerializer serializer)
{
var json = new Dictionary<string, JsonLanguageConfig>(value.Count);
foreach (LanguageConfig config in value)
{
json.Add(config.Language, new JsonLanguageConfig(config));
}
serializer.Serialize(writer, json);
}
protected override LanguagesConfig ReadValue(JsonReader reader, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, JsonLanguageConfig>>(reader);
var languagesConfig = new LanguageConfig[json.Count];
var i = 0;
foreach (var config in json)
{
languagesConfig[i++] = config.Value.ToConfig(config.Key);
}
return LanguagesConfig.Build(languagesConfig);
}
}
}

66
src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs

@ -0,0 +1,66 @@
// ==========================================================================
// LanguageConfig.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguageConfig : IFieldPartitionItem
{
private readonly Language language;
private readonly Language[] languageFallbacks;
private readonly bool isOptional;
public bool IsOptional
{
get { return isOptional; }
}
public Language Language
{
get { return language; }
}
public IEnumerable<Language> LanguageFallbacks
{
get { return languageFallbacks; }
}
string IFieldPartitionItem.Key
{
get { return language.Iso2Code; }
}
string IFieldPartitionItem.Name
{
get { return language.EnglishName; }
}
IEnumerable<string> IFieldPartitionItem.Fallback
{
get { return LanguageFallbacks.Select(x => x.Iso2Code); }
}
public LanguageConfig(Language language, bool isOptional = false, IEnumerable<Language> fallback = null)
: this(language, isOptional, fallback?.ToArray())
{
}
public LanguageConfig(Language language, bool isOptional = false, params Language[] fallback)
{
Guard.NotNull(language, nameof(language));
this.isOptional = isOptional;
this.language = language;
this.languageFallbacks = fallback ?? new Language[0];
}
}
}

178
src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -0,0 +1,178 @@
// ==========================================================================
// LanguagesConfig.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Squidex.Infrastructure;
#pragma warning disable IDE0016 // Use 'throw' expression
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguagesConfig : IFieldPartitioning
{
private State state;
public LanguageConfig Master
{
get { return state.Master; }
}
IFieldPartitionItem IFieldPartitioning.Master
{
get { return state.Master; }
}
IEnumerator IEnumerable.GetEnumerator()
{
return state.Languages.Values.GetEnumerator();
}
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator()
{
return state.Languages.Values.GetEnumerator();
}
public int Count
{
get { return state.Languages.Count; }
}
private LanguagesConfig(ICollection<LanguageConfig> configs)
{
Guard.NotNull(configs, nameof(configs));
state = new State(configs.ToImmutableDictionary(x => x.Language), configs.FirstOrDefault());
}
public static LanguagesConfig Build(params LanguageConfig[] configs)
{
Guard.NotNull(configs, nameof(configs));
return new LanguagesConfig(configs);
}
public static LanguagesConfig Build(params Language[] languages)
{
Guard.NotNull(languages, nameof(languages));
return new LanguagesConfig(languages.Select(x => new LanguageConfig(x, false)).ToList());
}
public void MakeMaster(Language language)
{
Guard.NotNull(language, nameof(language));
state = new State(state.Languages, state.Languages[language]);
}
public void Set(LanguageConfig config)
{
Guard.NotNull(config, nameof(config));
state = new State(state.Languages.SetItem(config.Language, config), state.Master?.Language == config.Language ? config : state.Master);
}
public void Remove(Language language)
{
Guard.NotNull(language, nameof(language));
var newLanguages =
state.Languages.Values.Where(x => x.Language != language)
.Select(config =>
{
return new LanguageConfig(
config.Language,
config.IsOptional,
config.LanguageFallbacks.Except(new[] { language }));
})
.ToImmutableDictionary(x => x.Language);
var newMaster =
state.Master.Language != language ?
state.Master :
state.Languages.Values.FirstOrDefault();
state = new State(newLanguages, newMaster);
}
public bool Contains(Language language)
{
return language != null && state.Languages.ContainsKey(language);
}
public bool TryGetConfig(Language language, out LanguageConfig config)
{
return state.Languages.TryGetValue(language, out config);
}
public bool TryGetItem(string key, out IFieldPartitionItem item)
{
if (Language.IsValidLanguage(key) && state.Languages.TryGetValue(key, out var value))
{
item = value;
return true;
}
else
{
item = null;
return false;
}
}
private sealed class State
{
public ImmutableDictionary<Language, LanguageConfig> Languages { get; }
public LanguageConfig Master { get; }
public State(ImmutableDictionary<Language, LanguageConfig> languages, LanguageConfig master)
{
foreach (var languageConfig in languages.Values)
{
foreach (var fallback in languageConfig.LanguageFallbacks)
{
if (!languages.ContainsKey(fallback))
{
var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'";
throw new InvalidOperationException(message);
}
}
}
Languages = languages;
if (master == null)
{
throw new InvalidOperationException("Config has no master language.");
}
this.Master = master;
}
}
public PartitionResolver ToResolver()
{
return partitioning =>
{
if (partitioning.Equals(Partitioning.Invariant))
{
return InvariantPartitioning.Instance;
}
return this;
};
}
}
}

30
src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs

@ -0,0 +1,30 @@
// ==========================================================================
// RoleExtension.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public static class RoleExtension
{
public static AppPermission ToAppPermission(this AppClientPermission clientPermission)
{
Guard.Enum(clientPermission, nameof(clientPermission));
return (AppPermission)Enum.Parse(typeof(AppPermission), clientPermission.ToString());
}
public static AppPermission ToAppPermission(this AppContributorPermission contributorPermission)
{
Guard.Enum(contributorPermission, nameof(contributorPermission));
return (AppPermission)Enum.Parse(typeof(AppPermission), contributorPermission.ToString());
}
}
}

89
src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs

@ -0,0 +1,89 @@
// ==========================================================================
// ContentData.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Contents
{
public abstract class ContentData<T> : Dictionary<T, ContentFieldData>, IEquatable<ContentData<T>>
{
public IEnumerable<KeyValuePair<T, ContentFieldData>> ValidValues
{
get { return this.Where(x => x.Value != null); }
}
protected ContentData(IEqualityComparer<T> comparer)
: base(comparer)
{
}
protected ContentData(IDictionary<T, ContentFieldData> copy, IEqualityComparer<T> comparer)
: base(copy, comparer)
{
}
protected static TResult Merge<TResult>(TResult source, TResult target) where TResult : ContentData<T>
{
if (ReferenceEquals(target, source))
{
return source;
}
foreach (var otherValue in source)
{
var fieldValue = target.GetOrAdd(otherValue.Key, x => new ContentFieldData());
foreach (var value in otherValue.Value)
{
fieldValue[value.Key] = value.Value;
}
}
return target;
}
protected static TResult Clean<TResult>(TResult source, TResult target) where TResult : ContentData<T>
{
foreach (var fieldValue in source.ValidValues)
{
var resultValue = new ContentFieldData();
foreach (var partitionValue in fieldValue.Value.Where(x => !x.Value.IsNull()))
{
resultValue[partitionValue.Key] = partitionValue.Value;
}
if (resultValue.Count > 0)
{
target[fieldValue.Key] = resultValue;
}
}
return target;
}
public override bool Equals(object obj)
{
return Equals(obj as ContentData<T>);
}
public bool Equals(ContentData<T> other)
{
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other));
}
public override int GetHashCode()
{
return this.DictionaryHashCode();
}
}
}

54
src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs

@ -0,0 +1,54 @@
// ==========================================================================
// ContentFieldData.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class ContentFieldData : Dictionary<string, JToken>, IEquatable<ContentFieldData>
{
private static readonly JTokenEqualityComparer JTokenEqualityComparer = new JTokenEqualityComparer();
public ContentFieldData()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public ContentFieldData AddValue(string key, JToken value)
{
Guard.NotNullOrEmpty(key, nameof(key));
this[key] = value;
return this;
}
public ContentFieldData AddValue(JToken value)
{
return AddValue(InvariantPartitioning.Instance.Master.Key, value);
}
public override bool Equals(object obj)
{
return Equals(obj as ContentFieldData);
}
public bool Equals(ContentFieldData other)
{
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other, EqualityComparer<string>.Default, JTokenEqualityComparer));
}
public override int GetHashCode()
{
return this.DictionaryHashCode(EqualityComparer<string>.Default, JTokenEqualityComparer);
}
}
}

51
src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs

@ -0,0 +1,51 @@
// ==========================================================================
// IdContentData.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class IdContentData : ContentData<long>, IEquatable<IdContentData>
{
public IdContentData()
: base(EqualityComparer<long>.Default)
{
}
public IdContentData(IdContentData copy)
: base(copy, EqualityComparer<long>.Default)
{
}
public IdContentData MergeInto(IdContentData target)
{
return Merge(this, target);
}
public IdContentData ToCleaned()
{
return Clean(this, new IdContentData());
}
public IdContentData AddField(long id, ContentFieldData data)
{
Guard.GreaterThan(id, 0, nameof(id));
this[id] = data;
return this;
}
public bool Equals(IdContentData other)
{
return base.Equals(other);
}
}
}

45
src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs

@ -0,0 +1,45 @@
// ==========================================================================
// NamedContentData.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class NamedContentData : ContentData<string>, IEquatable<NamedContentData>
{
public NamedContentData()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public NamedContentData MergeInto(NamedContentData target)
{
return Merge(this, target);
}
public NamedContentData ToCleaned()
{
return Clean(this, new NamedContentData());
}
public NamedContentData AddField(string name, ContentFieldData data)
{
Guard.NotNullOrEmpty(name, nameof(name));
this[name] = data;
return this;
}
public bool Equals(NamedContentData other)
{
return base.Equals(other);
}
}
}

17
src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Status.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Contents
{
public enum Status
{
Draft,
Archived,
Published
}
}

33
src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs

@ -0,0 +1,33 @@
// ==========================================================================
// StatusFlow.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusFlow
{
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[Status.Draft] = new[] { Status.Published, Status.Archived },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
};
public static bool Exists(Status status)
{
return Flow.ContainsKey(status);
}
public static bool CanChange(Status status, Status toStatus)
{
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus);
}
}
}

63
src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs

@ -0,0 +1,63 @@
// ==========================================================================
// DictionaryBase.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core
{
public abstract class DictionaryBase<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
private readonly Dictionary<TKey, TValue> inner = new Dictionary<TKey, TValue>();
public TValue this[TKey key]
{
get { return inner[key]; }
}
public IEnumerable<TKey> Keys
{
get { return inner.Keys; }
}
public IEnumerable<TValue> Values
{
get { return inner.Values; }
}
public int Count
{
get { return inner.Count; }
}
protected Dictionary<TKey, TValue> Inner
{
get { return inner; }
}
public bool ContainsKey(TKey key)
{
return inner.ContainsKey(key);
}
public bool TryGetValue(TKey key, out TValue value)
{
return inner.TryGetValue(key, out value);
}
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator()
{
return inner.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return inner.GetEnumerator();
}
}
}

14
src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs → src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs

@ -1,21 +1,23 @@
// ==========================================================================
// IAppClientEntity.cs
// IFieldPartitionItem.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Read.Apps
namespace Squidex.Domain.Apps.Core
{
public interface IAppClientEntity
public interface IFieldPartitionItem
{
string Key { get; }
string Name { get; }
string Secret { get; }
bool IsOptional { get; }
AppClientPermission Permission { get; }
IEnumerable<string> Fallback { get; }
}
}

19
src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs

@ -0,0 +1,19 @@
// ==========================================================================
// IFieldPartitioning.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core
{
public interface IFieldPartitioning : IReadOnlyCollection<IFieldPartitionItem>
{
IFieldPartitionItem Master { get; }
bool TryGetItem(string key, out IFieldPartitionItem item);
}
}

73
src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs

@ -0,0 +1,73 @@
// ==========================================================================
// InvariantPartitioning.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Domain.Apps.Core
{
public sealed class InvariantPartitioning : IFieldPartitioning, IFieldPartitionItem
{
public static readonly InvariantPartitioning Instance = new InvariantPartitioning();
public int Count
{
get { return 1; }
}
public IFieldPartitionItem Master
{
get { return this; }
}
string IFieldPartitionItem.Key
{
get { return "iv"; }
}
string IFieldPartitionItem.Name
{
get { return "Invariant"; }
}
bool IFieldPartitionItem.IsOptional
{
get { return false; }
}
IEnumerable<string> IFieldPartitionItem.Fallback
{
get { return Enumerable.Empty<string>(); }
}
private InvariantPartitioning()
{
}
public bool TryGetItem(string key, out IFieldPartitionItem item)
{
var isFound = string.Equals(key, "iv", StringComparison.OrdinalIgnoreCase);
item = isFound ? this : null;
return isFound;
}
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator()
{
yield return this;
}
IEnumerator IEnumerable.GetEnumerator()
{
yield return this;
}
}
}

50
src/Squidex.Domain.Apps.Core.Model/Partitioning.cs

@ -0,0 +1,50 @@
// ==========================================================================
// Partitioning.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core
{
public delegate IFieldPartitioning PartitionResolver(Partitioning key);
public sealed class Partitioning : IEquatable<Partitioning>
{
public static readonly Partitioning Invariant = new Partitioning("invariant");
public static readonly Partitioning Language = new Partitioning("language");
public string Key { get; }
public Partitioning(string key)
{
Guard.NotNullOrEmpty(key, nameof(key));
Key = key;
}
public override bool Equals(object obj)
{
return Equals(obj as Partitioning);
}
public bool Equals(Partitioning other)
{
return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase);
}
public override int GetHashCode()
{
return Key.GetHashCode();
}
public override string ToString()
{
return Key;
}
}
}

27
src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs

@ -0,0 +1,27 @@
// ==========================================================================
// PartitioningExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core
{
public static class PartitioningExtensions
{
private static readonly HashSet<string> AllowedPartitions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
Partitioning.Language.Key,
Partitioning.Invariant.Key
};
public static bool IsValidPartitioning(this string value)
{
return value == null || AllowedPartitions.Contains(value);
}
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// AssetsField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class AssetsField : Field<AssetsFieldProperties>
{
public AssetsField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new AssetsFieldProperties())
{
}
public AssetsField(long id, string name, Partitioning partitioning, AssetsFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

25
src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs

@ -0,0 +1,25 @@
// ==========================================================================
// AssetsFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(AssetsField))]
public sealed class AssetsFieldProperties : FieldProperties
{
public int? MinItems { get; set; }
public int? MaxItems { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// BooleanField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class BooleanField : Field<BooleanFieldProperties>
{
public BooleanField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new BooleanFieldProperties())
{
}
public BooleanField(long id, string name, Partitioning partitioning, BooleanFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs

@ -0,0 +1,16 @@
// ==========================================================================
// BooleanFieldEditor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum BooleanFieldEditor
{
Checkbox,
Toggle
}
}

25
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs

@ -0,0 +1,25 @@
// ==========================================================================
// BooleanFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(BooleanField))]
public sealed class BooleanFieldProperties : FieldProperties
{
public bool? DefaultValue { get; set; }
public BooleanFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs

@ -0,0 +1,16 @@
// ==========================================================================
// DateTimeCalculatedDefaultValue.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum DateTimeCalculatedDefaultValue
{
Now,
Today
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// DateTimeField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class DateTimeField : Field<DateTimeFieldProperties>
{
public DateTimeField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new DateTimeFieldProperties())
{
}
public DateTimeField(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs

@ -0,0 +1,16 @@
// ==========================================================================
// DateTimeFieldEditor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum DateTimeFieldEditor
{
Date,
DateTime
}
}

32
src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs

@ -0,0 +1,32 @@
// ==========================================================================
// DateTimeFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(DateTimeField))]
public sealed class DateTimeFieldProperties : FieldProperties
{
public Instant? MaxValue { get; set; }
public Instant? MinValue { get; set; }
public Instant? DefaultValue { get; set; }
public DateTimeFieldEditor Editor { get; set; }
public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

95
src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs

@ -0,0 +1,95 @@
// ==========================================================================
// Field.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class Field
{
private readonly long fieldId;
private readonly Partitioning partitioning;
private readonly string fieldName;
private bool isDisabled;
private bool isHidden;
private bool isLocked;
public long Id
{
get { return fieldId; }
}
public string Name
{
get { return fieldName; }
}
public bool IsLocked
{
get { return isLocked; }
}
public bool IsHidden
{
get { return isHidden; }
}
public bool IsDisabled
{
get { return isDisabled; }
}
public Partitioning Partitioning
{
get { return partitioning; }
}
public abstract FieldProperties RawProperties { get; }
protected Field(long id, string name, Partitioning partitioning)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(partitioning, nameof(partitioning));
Guard.GreaterThan(id, 0, nameof(id));
fieldId = id;
fieldName = name;
this.partitioning = partitioning;
}
public void Lock()
{
isLocked = true;
}
public void Hide()
{
isHidden = true;
}
public void Show()
{
isHidden = false;
}
public void Disable()
{
isDisabled = true;
}
public void Enable()
{
isDisabled = false;
}
public abstract void Update(FieldProperties newProperties);
public abstract T Accept<T>(IFieldVisitor<T> visitor);
}
}

21
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs

@ -0,0 +1,21 @@
// ==========================================================================
// FieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class FieldProperties : NamedElementPropertiesBase
{
public bool IsRequired { get; set; }
public bool IsListField { get; set; }
public string Placeholder { get; set; }
public abstract T Accept<T>(IFieldPropertiesVisitor<T> visitor);
}
}

116
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs

@ -0,0 +1,116 @@
// ==========================================================================
// FieldRegistry.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class FieldRegistry
{
private delegate Field FactoryFunction(long id, string name, Partitioning partitioning, FieldProperties properties);
private readonly TypeNameRegistry typeNameRegistry;
private readonly Dictionary<Type, Registered> fieldsByPropertyType = new Dictionary<Type, Registered>();
private sealed class Registered
{
private readonly FactoryFunction fieldFactory;
private readonly Type propertiesType;
public Type PropertiesType
{
get { return propertiesType; }
}
public Registered(FactoryFunction fieldFactory, Type propertiesType)
{
this.fieldFactory = fieldFactory;
this.propertiesType = propertiesType;
}
public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties)
{
return fieldFactory(id, name, partitioning, properties);
}
}
public FieldRegistry(TypeNameRegistry typeNameRegistry)
{
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
this.typeNameRegistry = typeNameRegistry;
Add<BooleanFieldProperties>(
(id, name, partitioning, properties) =>
new BooleanField(id, name, partitioning, (BooleanFieldProperties)properties));
Add<NumberFieldProperties>(
(id, name, partitioning, properties) =>
new NumberField(id, name, partitioning, (NumberFieldProperties)properties));
Add<StringFieldProperties>(
(id, name, partitioning, properties) =>
new StringField(id, name, partitioning, (StringFieldProperties)properties));
Add<JsonFieldProperties>(
(id, name, partitioning, properties) =>
new JsonField(id, name, partitioning, (JsonFieldProperties)properties));
Add<AssetsFieldProperties>(
(id, name, partitioning, properties) =>
new AssetsField(id, name, partitioning, (AssetsFieldProperties)properties));
Add<GeolocationFieldProperties>(
(id, name, partitioning, properties) =>
new GeolocationField(id, name, partitioning, (GeolocationFieldProperties)properties));
Add<ReferencesFieldProperties>(
(id, name, partitioning, properties) =>
new ReferencesField(id, name, partitioning, (ReferencesFieldProperties)properties));
Add<DateTimeFieldProperties>(
(id, name, partitioning, properties) =>
new DateTimeField(id, name, partitioning, (DateTimeFieldProperties)properties));
Add<TagsFieldProperties>(
(id, name, partitioning, properties) =>
new TagsField(id, name, partitioning, (TagsFieldProperties)properties));
typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime");
typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References");
}
private void Add<TFieldProperties>(FactoryFunction fieldFactory)
{
Guard.NotNull(fieldFactory, nameof(fieldFactory));
typeNameRegistry.Map(typeof(TFieldProperties));
var registered = new Registered(fieldFactory, typeof(TFieldProperties));
fieldsByPropertyType[registered.PropertiesType] = registered;
}
public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties)
{
Guard.NotNull(properties, nameof(properties));
var registered = fieldsByPropertyType.GetOrDefault(properties.GetType());
if (registered == null)
{
throw new InvalidOperationException($"The field property '{properties.GetType()}' is not supported.");
}
return registered.CreateField(id, name, partitioning, properties);
}
}
}

54
src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Field_Generic.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class Field<T> : Field where T : FieldProperties, new()
{
private T properties;
public T Properties
{
get { return properties; }
}
public override FieldProperties RawProperties
{
get { return properties; }
}
protected Field(long id, string name, Partitioning partitioning, T properties)
: base(id, name, partitioning)
{
Guard.NotNull(properties, nameof(properties));
this.properties = properties;
}
public override void Update(FieldProperties newProperties)
{
var typedProperties = ValidateProperties(newProperties);
properties = typedProperties;
}
private T ValidateProperties(FieldProperties newProperties)
{
Guard.NotNull(newProperties, nameof(newProperties));
if (!(newProperties is T typedProperties))
{
throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties));
}
return typedProperties;
}
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// GeolocationField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class GeolocationField : Field<GeolocationFieldProperties>
{
public GeolocationField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new GeolocationFieldProperties())
{
}
public GeolocationField(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

15
src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs

@ -0,0 +1,15 @@
// ==========================================================================
// GeolocationFieldEditor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum GeolocationFieldEditor
{
Map
}
}

23
src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs

@ -0,0 +1,23 @@
// ==========================================================================
// GeolocationFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(GeolocationField))]
public sealed class GeolocationFieldProperties : FieldProperties
{
public GeolocationFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

31
src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs

@ -0,0 +1,31 @@
// ==========================================================================
// IFieldPropertiesVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldPropertiesVisitor<out T>
{
T Visit(AssetsFieldProperties properties);
T Visit(BooleanFieldProperties properties);
T Visit(DateTimeFieldProperties properties);
T Visit(GeolocationFieldProperties properties);
T Visit(JsonFieldProperties properties);
T Visit(NumberFieldProperties properties);
T Visit(ReferencesFieldProperties properties);
T Visit(StringFieldProperties properties);
T Visit(TagsFieldProperties properties);
}
}

31
src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs

@ -0,0 +1,31 @@
// ==========================================================================
// IFieldVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldVisitor<out T>
{
T Visit(AssetsField field);
T Visit(BooleanField field);
T Visit(DateTimeField field);
T Visit(GeolocationField field);
T Visit(JsonField field);
T Visit(NumberField field);
T Visit(ReferencesField field);
T Visit(StringField field);
T Visit(TagsField field);
}
}

36
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs

@ -0,0 +1,36 @@
// ==========================================================================
// JsonFieldModel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json
{
public sealed class JsonFieldModel
{
[JsonProperty]
public long Id { get; set; }
[JsonProperty]
public bool IsHidden { get; set; }
[JsonProperty]
public bool IsLocked { get; set; }
[JsonProperty]
public bool IsDisabled { get; set; }
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public string Partitioning { get; set; }
[JsonProperty]
public FieldProperties Properties { get; set; }
}
}

99
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs

@ -0,0 +1,99 @@
// ==========================================================================
// JsonSchemaModel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json
{
public sealed class JsonSchemaModel
{
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public bool IsPublished { get; set; }
[JsonProperty]
public SchemaProperties Properties { get; set; }
[JsonProperty]
public List<JsonFieldModel> Fields { get; set; }
public JsonSchemaModel()
{
}
public JsonSchemaModel(Schema schema)
{
Name = schema.Name;
Properties = schema.Properties;
Fields =
schema.Fields?.Select(x =>
new JsonFieldModel
{
Id = x.Id,
Name = x.Name,
IsHidden = x.IsHidden,
IsLocked = x.IsLocked,
IsDisabled = x.IsDisabled,
Partitioning = x.Partitioning.Key,
Properties = x.RawProperties
}).ToList();
IsPublished = schema.IsPublished;
}
public Schema ToSchema(FieldRegistry fieldRegistry)
{
var schema = new Schema(Name);
if (Fields != null)
{
foreach (var fieldModel in Fields)
{
var parititonKey = new Partitioning(fieldModel.Partitioning);
var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties);
if (fieldModel.IsDisabled)
{
field.Disable();
}
if (fieldModel.IsLocked)
{
field.Lock();
}
if (fieldModel.IsHidden)
{
field.Hide();
}
schema.AddField(field);
}
}
if (IsPublished)
{
schema.Publish();
}
if (Properties != null)
{
schema.Update(Properties);
}
return schema;
}
}
}

36
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs

@ -0,0 +1,36 @@
// ==========================================================================
// SchemaConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json
{
public sealed class SchemaConverter : JsonClassConverter<Schema>
{
private readonly FieldRegistry fieldRegistry;
public SchemaConverter(FieldRegistry fieldRegistry)
{
Guard.NotNull(fieldRegistry, nameof(fieldRegistry));
this.fieldRegistry = fieldRegistry;
}
protected override void WriteValue(JsonWriter writer, Schema value, JsonSerializer serializer)
{
serializer.Serialize(writer, new JsonSchemaModel(value));
}
protected override Schema ReadValue(JsonReader reader, JsonSerializer serializer)
{
return serializer.Deserialize<JsonSchemaModel>(reader).ToSchema(fieldRegistry);
}
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// JsonField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class JsonField : Field<JsonFieldProperties>
{
public JsonField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new JsonFieldProperties())
{
}
public JsonField(long id, string name, Partitioning partitioning, JsonFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

21
src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs

@ -0,0 +1,21 @@
// ==========================================================================
// JsonFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(JsonField))]
public sealed class JsonFieldProperties : FieldProperties
{
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

17
src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs

@ -0,0 +1,17 @@
// ==========================================================================
// NamedElementPropertiesBase.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class NamedElementPropertiesBase
{
public string Label { get; set; }
public string Hints { get; set; }
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// NumberField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class NumberField : Field<NumberFieldProperties>
{
public NumberField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new NumberFieldProperties())
{
}
public NumberField(long id, string name, Partitioning partitioning, NumberFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

18
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs

@ -0,0 +1,18 @@
// ==========================================================================
// NumberFieldEditor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum NumberFieldEditor
{
Input,
Radio,
Dropdown,
Stars
}
}

31
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs

@ -0,0 +1,31 @@
// ==========================================================================
// NumberFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(NumberField))]
public sealed class NumberFieldProperties : FieldProperties
{
public double? MaxValue { get; set; }
public double? MinValue { get; set; }
public double? DefaultValue { get; set; }
public double[] AllowedValues { get; set; }
public NumberFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// ReferencesField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class ReferencesField : Field<ReferencesFieldProperties>
{
public ReferencesField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new ReferencesFieldProperties())
{
}
public ReferencesField(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs

@ -0,0 +1,28 @@
// ==========================================================================
// ReferencesFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(ReferencesField))]
public sealed class ReferencesFieldProperties : FieldProperties
{
public int? MinItems { get; set; }
public int? MaxItems { get; set; }
public Guid SchemaId { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

120
src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

@ -0,0 +1,120 @@
// ==========================================================================
// Schema.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class Schema
{
private readonly string name;
private readonly List<Field> fieldsOrdered = new List<Field>();
private readonly Dictionary<long, Field> fieldsById = new Dictionary<long, Field>();
private readonly Dictionary<string, Field> fieldsByName = new Dictionary<string, Field>();
private SchemaProperties properties = new SchemaProperties();
private bool isPublished;
public string Name
{
get { return name; }
}
public bool IsPublished
{
get { return isPublished; }
}
public IReadOnlyList<Field> Fields
{
get { return fieldsOrdered; }
}
public IReadOnlyDictionary<long, Field> FieldsById
{
get { return fieldsById; }
}
public IReadOnlyDictionary<string, Field> FieldsByName
{
get { return fieldsByName; }
}
public SchemaProperties Properties
{
get { return properties; }
}
public void Publish()
{
isPublished = true;
}
public void Unpublish()
{
isPublished = false;
}
public Schema(string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
this.name = name;
}
public void Update(SchemaProperties newProperties)
{
Guard.NotNull(newProperties, nameof(newProperties));
properties = newProperties;
}
public void DeleteField(long fieldId)
{
if (!fieldsById.TryGetValue(fieldId, out var field))
{
return;
}
fieldsById.Remove(fieldId);
fieldsByName.Remove(field.Name);
fieldsOrdered.Remove(field);
}
public void ReorderFields(List<long> ids)
{
Guard.NotNull(ids, nameof(ids));
if (ids.Count != fieldsOrdered.Count || ids.Any(x => !fieldsById.ContainsKey(x)))
{
throw new ArgumentException("Ids must cover all fields.", nameof(ids));
}
var fields = fieldsOrdered.ToList();
fieldsOrdered.Clear();
fieldsOrdered.AddRange(fields.OrderBy(f => ids.IndexOf(f.Id)));
}
public void AddField(Field field)
{
Guard.NotNull(field, nameof(field));
if (fieldsByName.ContainsKey(field.Name) || fieldsById.ContainsKey(field.Id))
{
throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field));
}
fieldsById.Add(field.Id, field);
fieldsByName.Add(field.Name, field);
fieldsOrdered.Add(field);
}
}
}

14
src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs

@ -0,0 +1,14 @@
// ==========================================================================
// SchemaProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class SchemaProperties : NamedElementPropertiesBase
{
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// StringField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class StringField : Field<StringFieldProperties>
{
public StringField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new StringFieldProperties())
{
}
public StringField(long id, string name, Partitioning partitioning, StringFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

20
src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs

@ -0,0 +1,20 @@
// ==========================================================================
// StringFieldEditor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public enum StringFieldEditor
{
Input,
Markdown,
Dropdown,
Radio,
RichText,
TextArea
}
}

35
src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -0,0 +1,35 @@
// ==========================================================================
// StringFieldProperties.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(StringField))]
public sealed class StringFieldProperties : FieldProperties
{
public int? MinLength { get; set; }
public int? MaxLength { get; set; }
public string DefaultValue { get; set; }
public string Pattern { get; set; }
public string PatternMessage { get; set; }
public string[] AllowedValues { get; set; }
public StringFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

28
src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs

@ -0,0 +1,28 @@
// ==========================================================================
// TagsField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class TagsField : Field<TagsFieldProperties>
{
public TagsField(long id, string name, Partitioning partitioning)
: base(id, name, partitioning, new TagsFieldProperties())
{
}
public TagsField(long id, string name, Partitioning partitioning, TagsFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
public override T Accept<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

25
src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs

@ -0,0 +1,25 @@
// ==========================================================================
// TagsField.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName(nameof(TagsField))]
public sealed class TagsFieldProperties : FieldProperties
{
public int? MinItems { get; set; }
public int? MaxItems { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

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

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

25
src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs

@ -0,0 +1,25 @@
// ==========================================================================
// WebhookSchema.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Core.Webhooks
{
public sealed class WebhookSchema
{
public Guid SchemaId { get; set; }
public bool SendCreate { get; set; }
public bool SendUpdate { get; set; }
public bool SendDelete { get; set; }
public bool SendPublish { get; set; }
}
}

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

@ -0,0 +1,202 @@
// ==========================================================================
// ContentConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
public static class ContentConverter
{
public static NamedContentData ToNameModel(this IdContentData source, Schema schema, bool decodeJsonField)
{
Guard.NotNull(schema, nameof(schema));
var result = new NamedContentData();
foreach (var fieldValue in source)
{
if (!schema.FieldsById.TryGetValue(fieldValue.Key, out var field))
{
continue;
}
if (decodeJsonField && field is JsonField)
{
var encodedValue = new ContentFieldData();
foreach (var partitionValue in fieldValue.Value)
{
if (partitionValue.Value.IsNull())
{
encodedValue[partitionValue.Key] = null;
}
else
{
var value = Encoding.UTF8.GetString(Convert.FromBase64String(partitionValue.Value.ToString()));
encodedValue[partitionValue.Key] = JToken.Parse(value);
}
}
result[field.Name] = encodedValue;
}
else
{
result[field.Name] = fieldValue.Value;
}
}
return result;
}
public static IdContentData ToIdModel(this NamedContentData content, Schema schema, bool encodeJsonField)
{
Guard.NotNull(schema, nameof(schema));
var result = new IdContentData();
foreach (var fieldValue in content)
{
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field))
{
continue;
}
var fieldId = field.Id;
if (encodeJsonField && field is JsonField)
{
var encodedValue = new ContentFieldData();
foreach (var partitionValue in fieldValue.Value)
{
if (partitionValue.Value.IsNull())
{
encodedValue[partitionValue.Key] = null;
}
else
{
var value = Convert.ToBase64String(Encoding.UTF8.GetBytes(partitionValue.Value.ToString()));
encodedValue[partitionValue.Key] = value;
}
}
result[fieldId] = encodedValue;
}
else
{
result[fieldId] = fieldValue.Value;
}
}
return result;
}
public static NamedContentData ToApiModel(this NamedContentData content, Schema schema, LanguagesConfig languagesConfig, bool excludeHidden = true)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(languagesConfig, nameof(languagesConfig));
var codeForInvariant = InvariantPartitioning.Instance.Master.Key;
var codeForMasterLanguage = languagesConfig.Master.Language.Iso2Code;
var result = new NamedContentData();
foreach (var fieldValue in content)
{
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field) || (excludeHidden && field.IsHidden))
{
continue;
}
var fieldResult = new ContentFieldData();
var fieldValues = fieldValue.Value;
if (field.Partitioning.Equals(Partitioning.Language))
{
foreach (var languageConfig in languagesConfig)
{
var languageCode = languageConfig.Key;
if (fieldValues.TryGetValue(languageCode, out var value))
{
fieldResult.Add(languageCode, value);
}
else if (languageConfig == languagesConfig.Master && fieldValues.TryGetValue(codeForInvariant, out value))
{
fieldResult.Add(languageCode, value);
}
}
}
else
{
if (fieldValues.TryGetValue(codeForInvariant, out var value))
{
fieldResult.Add(codeForInvariant, value);
}
else if (fieldValues.TryGetValue(codeForMasterLanguage, out value))
{
fieldResult.Add(codeForInvariant, value);
}
else if (fieldValues.Count > 0)
{
fieldResult.Add(codeForInvariant, fieldValues.Values.First());
}
}
result.Add(field.Name, fieldResult);
}
return result;
}
public static object ToLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection<Language> languagePreferences = null)
{
Guard.NotNull(languagesConfig, nameof(languagesConfig));
if (languagePreferences == null || languagePreferences.Count == 0)
{
return content;
}
if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig))
{
languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList();
}
var result = new Dictionary<string, JToken>();
foreach (var fieldValue in content)
{
var fieldValues = fieldValue.Value;
foreach (var language in languagePreferences)
{
if (fieldValues.TryGetValue(language, out var value) && value != null)
{
result[fieldValue.Key] = value;
break;
}
}
}
return result;
}
}
}

78
src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs

@ -0,0 +1,78 @@
// ==========================================================================
// ContentEnricher.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.EnrichContent
{
public sealed class ContentEnricher
{
private readonly Schema schema;
private readonly PartitionResolver partitionResolver;
public ContentEnricher(Schema schema, PartitionResolver partitionResolver)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(partitionResolver, nameof(partitionResolver));
this.schema = schema;
this.partitionResolver = partitionResolver;
}
public void Enrich(NamedContentData data)
{
Guard.NotNull(data, nameof(data));
foreach (var field in schema.Fields)
{
var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData());
var fieldPartition = partitionResolver(field.Partitioning);
foreach (var partitionItem in fieldPartition)
{
Enrich(field, fieldData, partitionItem);
}
if (fieldData.Count > 0)
{
data[field.Name] = fieldData;
}
}
}
private static void Enrich(Field field, ContentFieldData fieldData, IFieldPartitionItem partitionItem)
{
Guard.NotNull(fieldData, nameof(fieldData));
var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant());
if (field.RawProperties.IsRequired || defaultValue.IsNull())
{
return;
}
var key = partitionItem.Key;
if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value))
{
fieldData.AddValue(key, defaultValue);
}
}
private static bool ShouldApplyDefaultValue(Field field, JToken value)
{
return value.IsNull() || (field is StringField && value is JValue jValue && Equals(jValue.Value, string.Empty));
}
}
}

23
src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs

@ -0,0 +1,23 @@
// ==========================================================================
// ContentExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.EnrichContent
{
public static class ContentEnrichmentExtensions
{
public static void Enrich(this NamedContentData data, Schema schema, PartitionResolver partitionResolver)
{
var enricher = new ContentEnricher(schema, partitionResolver);
enricher.Enrich(data);
}
}
}

87
src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs

@ -0,0 +1,87 @@
// ==========================================================================
// ValidatorsFactory.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.EnrichContent
{
public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JToken>
{
private readonly Instant now;
private DefaultValueFactory(Instant now)
{
this.now = now;
}
public static JToken CreateDefaultValue(Field field, Instant now)
{
Guard.NotNull(field, nameof(field));
return field.RawProperties.Accept(new DefaultValueFactory(now));
}
public JToken Visit(AssetsFieldProperties properties)
{
return new JArray();
}
public JToken Visit(BooleanFieldProperties properties)
{
return properties.DefaultValue;
}
public JToken Visit(GeolocationFieldProperties properties)
{
return JValue.CreateNull();
}
public JToken Visit(JsonFieldProperties properties)
{
return JValue.CreateNull();
}
public JToken Visit(NumberFieldProperties properties)
{
return properties.DefaultValue;
}
public JToken Visit(ReferencesFieldProperties properties)
{
return new JArray();
}
public JToken Visit(StringFieldProperties properties)
{
return properties.DefaultValue;
}
public JToken Visit(TagsFieldProperties properties)
{
return new JArray();
}
public JToken Visit(DateTimeFieldProperties properties)
{
if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now)
{
return now.ToString();
}
if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today)
{
return now.ToString().Substring(10);
}
return properties.DefaultValue?.ToString();
}
}
}

75
src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs

@ -0,0 +1,75 @@
// ==========================================================================
// ContentReferencesExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ContentReferencesExtensions
{
public static IdContentData ToCleanedReferences(this IdContentData source, Schema schema, ISet<Guid> deletedReferencedIds)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(deletedReferencedIds, nameof(deletedReferencedIds));
var result = new IdContentData(source);
foreach (var field in schema.Fields)
{
var fieldData = source.GetOrDefault(field.Id);
if (fieldData == null)
{
continue;
}
foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull()).ToList())
{
var newValue = field.CleanReferences(partitionValue.Value, deletedReferencedIds);
fieldData[partitionValue.Key] = newValue;
}
}
return result;
}
public static IEnumerable<Guid> GetReferencedIds(this IdContentData source, Schema schema)
{
Guard.NotNull(schema, nameof(schema));
var foundReferences = new HashSet<Guid>();
foreach (var field in schema.Fields)
{
var fieldData = source.GetOrDefault(field.Id);
if (fieldData == null)
{
continue;
}
foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull()))
{
var ids = field.ExtractReferences(partitionValue.Value);
foreach (var id in ids.Where(x => foundReferences.Add(x)))
{
yield return id;
}
}
}
}
}
}

58
src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs

@ -0,0 +1,58 @@
// ==========================================================================
// ReferenceExtractor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ReferencesCleaner
{
public static JToken CleanReferences(this Field field, JToken value, ISet<Guid> oldReferences)
{
if ((field is AssetsField || field is ReferencesField) && !value.IsNull())
{
switch (field)
{
case AssetsField assetsField:
return Visit(assetsField, value, oldReferences);
case ReferencesField referencesField:
return Visit(referencesField, value, oldReferences);
}
}
return value;
}
private static JToken Visit(AssetsField field, JToken value, IEnumerable<Guid> oldReferences)
{
var oldIds = field.ExtractReferences(value).ToList();
var newIds = oldIds.Except(oldReferences).ToList();
return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value;
}
private static JToken Visit(ReferencesField field, JToken value, ICollection<Guid> oldReferences)
{
if (oldReferences.Contains(field.Properties.SchemaId))
{
return new JArray();
}
var oldIds = field.ExtractReferences(value).ToList();
var newIds = oldIds.Except(oldReferences).ToList();
return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value;
}
}
}

63
src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs

@ -0,0 +1,63 @@
// ==========================================================================
// ReferenceExtractor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ReferencesExtractor
{
public static IEnumerable<Guid> ExtractReferences(this Field field, JToken value)
{
switch (field)
{
case AssetsField assetsField:
return Visit(assetsField, value);
case ReferencesField referencesField:
return Visit(referencesField, value);
}
return Enumerable.Empty<Guid>();
}
public static IEnumerable<Guid> Visit(AssetsField field, JToken value)
{
IEnumerable<Guid> result = null;
try
{
result = value?.ToObject<List<Guid>>();
}
catch
{
result = null;
}
return result ?? Enumerable.Empty<Guid>();
}
private static IEnumerable<Guid> Visit(ReferencesField field, JToken value)
{
IEnumerable<Guid> result = null;
try
{
result = value?.ToObject<List<Guid>>() ?? Enumerable.Empty<Guid>();
}
catch
{
result = Enumerable.Empty<Guid>();
}
return result.Union(new[] { field.Properties.SchemaId });
}
}
}

61
src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs

@ -0,0 +1,61 @@
// ==========================================================================
// EdmSchemaExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{
public static class EdmSchemaExtensions
{
public static string EscapeEdmField(this string field)
{
return field.Replace("-", "_");
}
public static string UnescapeEdmField(this string field)
{
return field.Replace("_", "-");
}
public static EdmComplexType BuildEdmType(this Schema schema, PartitionResolver partitionResolver, Func<EdmComplexType, EdmComplexType> typeResolver)
{
Guard.NotNull(typeResolver, nameof(typeResolver));
Guard.NotNull(partitionResolver, nameof(partitionResolver));
var schemaName = schema.Name.ToPascalCase();
var edmType = new EdmComplexType("Squidex", schemaName);
foreach (var field in schema.FieldsByName.Values.Where(x => !x.IsHidden))
{
var edmValueType = EdmTypeVisitor.CreateEdmType(field);
if (edmValueType == null)
{
continue;
}
var partitionType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}{field.Name.ToPascalCase()}Property"));
var partition = partitionResolver(field.Partitioning);
foreach (var partitionItem in partition)
{
partitionType.AddStructuralProperty(partitionItem.Key, edmValueType);
}
edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false));
}
return edmType;
}
}
}

77
src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs

@ -0,0 +1,77 @@
// ==========================================================================
// EdmTypeVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.OData.Edm;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
{
public sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference>
{
private static readonly EdmTypeVisitor Instance = new EdmTypeVisitor();
private EdmTypeVisitor()
{
}
public static IEdmTypeReference CreateEdmType(Field field)
{
return field.Accept(Instance);
}
public IEdmTypeReference Visit(AssetsField field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
public IEdmTypeReference Visit(BooleanField field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field);
}
public IEdmTypeReference Visit(DateTimeField field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field);
}
public IEdmTypeReference Visit(GeolocationField field)
{
return null;
}
public IEdmTypeReference Visit(JsonField field)
{
return null;
}
public IEdmTypeReference Visit(NumberField field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.Double, field);
}
public IEdmTypeReference Visit(ReferencesField field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
public IEdmTypeReference Visit(StringField field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
public IEdmTypeReference Visit(TagsField field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);
}
private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, Field field)
{
return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired);
}
}
}

57
src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs

@ -0,0 +1,57 @@
// ==========================================================================
// ContentSchemaBuilder.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using NJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public sealed class ContentSchemaBuilder
{
public JsonSchema4 CreateContentSchema(Schema schema, JsonSchema4 dataSchema)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(dataSchema, nameof(dataSchema));
var schemaName = schema.Properties.Label.WithFallback(schema.Name);
var contentSchema = new JsonSchema4
{
Properties =
{
["id"] = CreateProperty($"The id of the {schemaName} content."),
["data"] = CreateProperty($"The data of the {schemaName}.", dataSchema),
["version"] = CreateProperty($"The version of the {schemaName}.", JsonObjectType.Number),
["created"] = CreateProperty($"The date and time when the {schemaName} content has been created.", "date-time"),
["createdBy"] = CreateProperty($"The user that has created the {schemaName} content."),
["lastModified"] = CreateProperty($"The date and time when the {schemaName} content has been modified last.", "date-time"),
["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.")
},
Type = JsonObjectType.Object
};
return contentSchema;
}
private static JsonProperty CreateProperty(string description, JsonSchema4 dataSchema)
{
return new JsonProperty { Description = description, IsRequired = true, Type = JsonObjectType.Object, Reference = dataSchema };
}
private static JsonProperty CreateProperty(string description, JsonObjectType type)
{
return new JsonProperty { Description = description, IsRequired = true, Type = type };
}
private static JsonProperty CreateProperty(string description, string format = null)
{
return new JsonProperty { Description = description, Format = format, IsRequired = true, Type = JsonObjectType.String };
}
}
}

72
src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs

@ -0,0 +1,72 @@
// ==========================================================================
// JsonSchemaExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using NJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public static class JsonSchemaExtensions
{
public static JsonSchema4 BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{
Guard.NotNull(schemaResolver, nameof(schemaResolver));
Guard.NotNull(partitionResolver, nameof(partitionResolver));
var schemaName = schema.Name.ToPascalCase();
var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver);
var jsonSchema = new JsonSchema4 { Type = JsonObjectType.Object };
foreach (var field in schema.Fields.Where(x => !x.IsHidden))
{
var partitionProperty = CreateProperty(field);
var partitionObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false };
var partition = partitionResolver(field.Partitioning);
foreach (var partitionItem in partition)
{
var partitionItemProperty = field.Accept(jsonTypeVisitor);
partitionItemProperty.Description = partitionItem.Name;
partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty);
}
partitionProperty.Reference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject);
jsonSchema.Properties.Add(field.Name, partitionProperty);
}
return jsonSchema;
}
public static JsonProperty CreateProperty(Field field)
{
var jsonProperty = new JsonProperty { IsRequired = field.RawProperties.IsRequired, Type = JsonObjectType.Object };
if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints))
{
jsonProperty.Description = field.RawProperties.Hints;
}
else
{
jsonProperty.Description = field.Name;
}
if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints))
{
jsonProperty.Description += $" ({field.RawProperties.Hints}).";
}
return jsonProperty;
}
}
}

163
src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs

@ -0,0 +1,163 @@
// ==========================================================================
// JsonTypeVisitor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.ObjectModel;
using NJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{
public sealed class JsonTypeVisitor : IFieldVisitor<JsonProperty>
{
private readonly Func<string, JsonSchema4, JsonSchema4> schemaResolver;
public JsonTypeVisitor(Func<string, JsonSchema4, JsonSchema4> schemaResolver)
{
this.schemaResolver = schemaResolver;
}
public JsonProperty Visit(AssetsField field)
{
return CreateProperty(field, jsonProperty =>
{
var itemSchema = schemaResolver("AssetItem", new JsonSchema4 { Type = JsonObjectType.String });
jsonProperty.Type = JsonObjectType.Array;
jsonProperty.Item = itemSchema;
});
}
public JsonProperty Visit(BooleanField field)
{
return CreateProperty(field, jsonProperty =>
{
jsonProperty.Type = JsonObjectType.Boolean;
});
}
public JsonProperty Visit(DateTimeField field)
{
return CreateProperty(field, jsonProperty =>
{
jsonProperty.Type = JsonObjectType.String;
jsonProperty.Format = JsonFormatStrings.DateTime;
});
}
public JsonProperty Visit(GeolocationField field)
{
return CreateProperty(field, jsonProperty =>
{
var geolocationSchema = new JsonSchema4
{
AllowAdditionalProperties = false
};
geolocationSchema.Properties.Add("latitude", new JsonProperty
{
Type = JsonObjectType.Number,
Minimum = -90,
Maximum = 90,
IsRequired = true
});
geolocationSchema.Properties.Add("longitude", new JsonProperty
{
Type = JsonObjectType.Number,
Minimum = -180,
Maximum = 180,
IsRequired = true
});
var schemaReference = schemaResolver("GeolocationDto", geolocationSchema);
jsonProperty.Type = JsonObjectType.Object;
jsonProperty.Reference = schemaReference;
});
}
public JsonProperty Visit(JsonField field)
{
return CreateProperty(field, jsonProperty =>
{
jsonProperty.Type = JsonObjectType.Object;
});
}
public JsonProperty Visit(NumberField field)
{
return CreateProperty(field, jsonProperty =>
{
jsonProperty.Type = JsonObjectType.Number;
if (field.Properties.MinValue.HasValue)
{
jsonProperty.Minimum = (decimal)field.Properties.MinValue.Value;
}
if (field.Properties.MaxValue.HasValue)
{
jsonProperty.Maximum = (decimal)field.Properties.MaxValue.Value;
}
});
}
public JsonProperty Visit(ReferencesField field)
{
return CreateProperty(field, jsonProperty =>
{
var itemSchema = schemaResolver("ReferenceItem", new JsonSchema4 { Type = JsonObjectType.String });
jsonProperty.Type = JsonObjectType.Array;
jsonProperty.Item = itemSchema;
});
}
public JsonProperty Visit(StringField field)
{
return CreateProperty(field, jsonProperty =>
{
jsonProperty.Type = JsonObjectType.String;
jsonProperty.MinLength = field.Properties.MinLength;
jsonProperty.MaxLength = field.Properties.MaxLength;
if (field.Properties.AllowedValues != null)
{
var names = jsonProperty.EnumerationNames = jsonProperty.EnumerationNames ?? new Collection<string>();
foreach (var value in field.Properties.AllowedValues)
{
names.Add(value);
}
}
});
}
public JsonProperty Visit(TagsField field)
{
return CreateProperty(field, jsonProperty =>
{
var itemSchema = schemaResolver("TagsItem", new JsonSchema4 { Type = JsonObjectType.String });
jsonProperty.Type = JsonObjectType.Array;
jsonProperty.Item = itemSchema;
});
}
private static JsonProperty CreateProperty(Field field, Action<JsonProperty> updater)
{
var property = new JsonProperty { IsRequired = field.RawProperties.IsRequired };
updater(property);
return property;
}
}
}

132
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -0,0 +1,132 @@
// ==========================================================================
// ContentDataObject.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
#pragma warning disable RECS0133 // Parameter name differs in base declaration
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentDataObject : ObjectInstance
{
private readonly NamedContentData contentData;
private HashSet<string> fieldsToDelete;
private Dictionary<string, ContentDataProperty> fieldProperties;
private bool isChanged;
public ContentDataObject(Engine engine, NamedContentData contentData)
: base(engine)
{
Extensible = true;
this.contentData = contentData;
}
public void MarkChanged()
{
isChanged = true;
}
public bool TryUpdate(out NamedContentData result)
{
result = contentData;
if (isChanged)
{
if (fieldsToDelete != null)
{
foreach (var field in fieldsToDelete)
{
contentData.Remove(field);
}
}
if (fieldProperties != null)
{
foreach (var kvp in fieldProperties)
{
if (kvp.Value.ContentField.TryUpdate(out var fieldData))
{
contentData[kvp.Key] = fieldData;
}
}
}
}
return isChanged;
}
public override void RemoveOwnProperty(string propertyName)
{
if (fieldsToDelete == null)
{
fieldsToDelete = new HashSet<string>();
}
fieldsToDelete.Add(propertyName);
fieldProperties?.Remove(propertyName);
MarkChanged();
}
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError)
{
EnsurePropertiesInitialized();
if (!fieldProperties.ContainsKey(propertyName))
{
fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value };
}
return true;
}
public override void Put(string propertyName, JsValue value, bool throwOnError)
{
EnsurePropertiesInitialized();
fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this)).Value = value;
}
public override PropertyDescriptor GetOwnProperty(string propertyName)
{
EnsurePropertiesInitialized();
return fieldProperties.GetOrDefault(propertyName) ?? new PropertyDescriptor(new ObjectInstance(Engine) { Extensible = true }, true, false, true);
}
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties()
{
EnsurePropertiesInitialized();
foreach (var property in fieldProperties)
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
}
private void EnsurePropertiesInitialized()
{
if (fieldProperties == null)
{
fieldProperties = new Dictionary<string, ContentDataProperty>(contentData.Count);
foreach (var kvp in contentData)
{
fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false)));
}
}
}
}
}

68
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs

@ -0,0 +1,68 @@
// ==========================================================================
// ContentFieldProperty.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentDataProperty : PropertyDescriptor
{
private readonly ContentDataObject contentData;
private ContentFieldObject contentField;
private JsValue value;
public override JsValue Value
{
get
{
return value;
}
set
{
if (!Equals(this.value, value))
{
if (value == null || !value.IsObject())
{
throw new JavaScriptException("Can only assign object to content data.");
}
var obj = value.AsObject();
contentField = new ContentFieldObject(contentData, new ContentFieldData(), true);
foreach (var kvp in obj.GetOwnProperties())
{
contentField.Put(kvp.Key, kvp.Value.Value, true);
}
this.value = new JsValue(contentField);
}
}
}
public ContentFieldObject ContentField
{
get { return contentField; }
}
public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null)
: base(null, true, true, true)
{
this.contentData = contentData;
this.contentField = contentField;
if (contentField != null)
{
value = new JsValue(contentField);
}
}
}
}

137
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs

@ -0,0 +1,137 @@
// ==========================================================================
// ContentFieldObject.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
#pragma warning disable RECS0133 // Parameter name differs in base declaration
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentFieldObject : ObjectInstance
{
private readonly ContentDataObject contentData;
private readonly ContentFieldData fieldData;
private HashSet<string> valuesToDelete;
private Dictionary<string, ContentFieldProperty> valueProperties;
private bool isChanged;
public ContentFieldData FieldData
{
get { return fieldData; }
}
public ContentFieldObject(ContentDataObject contentData, ContentFieldData fieldData, bool isNew)
: base(contentData.Engine)
{
Extensible = true;
this.contentData = contentData;
this.fieldData = fieldData;
if (isNew)
{
MarkChanged();
}
}
public void MarkChanged()
{
isChanged = true;
contentData.MarkChanged();
}
public bool TryUpdate(out ContentFieldData result)
{
result = fieldData;
if (isChanged)
{
if (valuesToDelete != null)
{
foreach (var field in valuesToDelete)
{
fieldData.Remove(field);
}
}
if (valueProperties != null)
{
foreach (var kvp in valueProperties)
{
if (kvp.Value.IsChanged)
{
fieldData[kvp.Key] = kvp.Value.ContentValue;
}
}
}
}
return isChanged;
}
public override void RemoveOwnProperty(string propertyName)
{
if (valuesToDelete == null)
{
valuesToDelete = new HashSet<string>();
}
valuesToDelete.Add(propertyName);
valueProperties?.Remove(propertyName);
MarkChanged();
}
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError)
{
EnsurePropertiesInitialized();
if (!valueProperties.ContainsKey(propertyName))
{
valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value };
}
return true;
}
public override PropertyDescriptor GetOwnProperty(string propertyName)
{
EnsurePropertiesInitialized();
return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined;
}
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties()
{
EnsurePropertiesInitialized();
foreach (var property in valueProperties)
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
}
private void EnsurePropertiesInitialized()
{
if (valueProperties == null)
{
valueProperties = new Dictionary<string, ContentFieldProperty>(FieldData.Count);
foreach (var kvp in FieldData)
{
valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value));
}
}
}
}
}

59
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs

@ -0,0 +1,59 @@
// ==========================================================================
// ContentFieldProperty.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Jint.Native;
using Jint.Runtime.Descriptors;
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentFieldProperty : PropertyDescriptor
{
private readonly ContentFieldObject contentField;
private JToken contentValue;
private JsValue value;
private bool isChanged;
public override JsValue Value
{
get
{
return value ?? (value = JsonMapper.Map(contentValue, contentField.Engine));
}
set
{
if (!Equals(this.value, value))
{
this.value = value;
contentValue = null;
contentField.MarkChanged();
isChanged = true;
}
}
}
public JToken ContentValue
{
get { return contentValue ?? (contentValue = JsonMapper.Map(value)); }
}
public bool IsChanged
{
get { return isChanged; }
}
public ContentFieldProperty(ContentFieldObject contentField, JToken contentValue = null)
: base(null, true, true, true)
{
this.contentField = contentField;
this.contentValue = contentValue;
}
}
}

146
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs

@ -0,0 +1,146 @@
// ==========================================================================
// JsonMapper.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public static class JsonMapper
{
public static JsValue Map(JToken value, Engine engine)
{
if (value == null)
{
return JsValue.Null;
}
switch (value.Type)
{
case JTokenType.Date:
case JTokenType.Guid:
case JTokenType.String:
case JTokenType.Uri:
case JTokenType.TimeSpan:
return new JsValue((string)value);
case JTokenType.Null:
return JsValue.Null;
case JTokenType.Undefined:
return JsValue.Undefined;
case JTokenType.Integer:
return new JsValue((long)value);
case JTokenType.Float:
return new JsValue((double)value);
case JTokenType.Boolean:
return new JsValue((bool)value);
case JTokenType.Object:
return FromObject(value, engine);
case JTokenType.Array:
{
var arr = (JArray)value;
var target = new JsValue[arr.Count];
for (var i = 0; i < arr.Count; i++)
{
target[i] = Map(arr[i], engine);
}
return engine.Array.Construct(target);
}
}
throw new ArgumentException("Invalid json type.", nameof(value));
}
private static JsValue FromObject(JToken value, Engine engine)
{
var obj = (JObject)value;
var target = new ObjectInstance(engine);
foreach (var property in obj)
{
target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true);
}
return target;
}
public static JToken Map(JsValue value)
{
if (value == null || value.IsNull())
{
return JValue.CreateNull();
}
if (value.IsUndefined())
{
return JValue.CreateUndefined();
}
if (value.IsString())
{
return new JValue(value.AsString());
}
if (value.IsBoolean())
{
return new JValue(value.AsBoolean());
}
if (value.IsNumber())
{
return new JValue(value.AsNumber());
}
if (value.IsDate())
{
return new JValue(value.AsDate().ToDateTime());
}
if (value.IsRegExp())
{
return JValue.CreateString(value.AsRegExp().Value?.ToString());
}
if (value.IsArray())
{
var arr = value.AsArray();
var target = new JArray();
for (var i = 0; i < arr.GetLength(); i++)
{
target.Add(Map(arr.Get(i.ToString())));
}
return target;
}
if (value.IsObject())
{
var obj = value.AsObject();
var target = new JObject();
foreach (var kvp in obj.GetOwnProperties())
{
target[kvp.Key] = Map(kvp.Value.Value);
}
return target;
}
throw new ArgumentException("Invalid json type.", nameof(value));
}
}
}

21
src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs

@ -0,0 +1,21 @@
// ==========================================================================
// IScriptEngine.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public interface IScriptEngine
{
void Execute(ScriptContext context, string script);
NamedContentData ExecuteAndTransform(ScriptContext context, string script);
NamedContentData Transform(ScriptContext context, string script);
}
}

178
src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -0,0 +1,178 @@
// ==========================================================================
// JintScriptEngine.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Jint;
using Jint.Native.Object;
using Jint.Parser;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class JintScriptEngine : IScriptEngine
{
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200);
public void Execute(ScriptContext context, string script)
{
Guard.NotNull(context, nameof(context));
if (!string.IsNullOrWhiteSpace(script))
{
var engine = CreateScriptEngine(context);
EnableDisallow(engine);
EnableReject(engine);
Execute(engine, script);
}
}
public NamedContentData ExecuteAndTransform(ScriptContext context, string script)
{
Guard.NotNull(context, nameof(context));
var result = context.Data;
if (!string.IsNullOrWhiteSpace(script))
{
var engine = CreateScriptEngine(context);
EnableDisallow(engine);
EnableReject(engine);
engine.SetValue("operation", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
data.TryUpdate(out result);
}
}));
engine.SetValue("replace", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
data.TryUpdate(out result);
}
}));
Execute(engine, script);
}
return result;
}
public NamedContentData Transform(ScriptContext context, string script)
{
Guard.NotNull(context, nameof(context));
var result = context.Data;
if (!string.IsNullOrWhiteSpace(script))
{
try
{
var engine = CreateScriptEngine(context);
engine.SetValue("replace", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
data.TryUpdate(out result);
}
}));
engine.Execute(script);
}
catch (Exception)
{
result = context.Data;
}
}
return result;
}
private static void Execute(Engine engine, string script)
{
try
{
engine.Execute(script);
}
catch (ParserException ex)
{
throw new ValidationException($"Failed to execute script with javascript syntaxs error.", new ValidationError(ex.Message));
}
catch (JavaScriptException ex)
{
throw new ValidationException($"Failed to execute script with javascript error.", new ValidationError(ex.Message));
}
}
private Engine CreateScriptEngine(ScriptContext context)
{
var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict());
var contextInstance = new ObjectInstance(engine);
if (context.Data != null)
{
contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true);
}
if (context.OldData != null)
{
contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.OldData), true, true, true);
}
if (context.User != null)
{
contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false);
}
if (!string.IsNullOrWhiteSpace(context.Operation))
{
contextInstance.FastAddProperty("operation", context.Operation, false, true, false);
}
engine.SetValue("ctx", contextInstance);
return engine;
}
private static void EnableDisallow(Engine engine)
{
engine.SetValue("disallow", new Action<string>(message =>
{
var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed";
throw new DomainForbiddenException(exMessage);
}));
}
private static void EnableReject(Engine engine)
{
engine.SetValue("reject", new Action<string>(message =>
{
var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null;
throw new ValidationException($"Script rejected the operation.", errors);
}));
}
}
}

50
src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs

@ -0,0 +1,50 @@
// ==========================================================================
// JintUser.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using System.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class JintUser : ObjectInstance
{
public JintUser(Engine engine, ClaimsPrincipal principal)
: base(engine)
{
var subjectId = principal.OpenIdSubject();
var isClient = string.IsNullOrWhiteSpace(subjectId);
if (!isClient)
{
FastAddProperty("id", subjectId, false, true, false);
FastAddProperty("isClient", false, false, true, false);
}
else
{
FastAddProperty("id", principal.OpenIdClientId(), false, true, false);
FastAddProperty("isClient", true, false, true, false);
}
FastAddProperty("email", principal.OpenIdEmail(), false, true, false);
var claimsInstance = new ObjectInstance(engine);
foreach (var group in principal.Claims.GroupBy(x => x.Type))
{
claimsInstance.FastAddProperty(group.Key, engine.Array.Construct(group.Select(x => new JsValue(x.Value)).ToArray()), false, true, false);
}
FastAddProperty("claims", claimsInstance, false, true, false);
}
}
}

27
src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs

@ -0,0 +1,27 @@
// ==========================================================================
// ScriptContext.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptContext
{
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; }
public NamedContentData Data { get; set; }
public NamedContentData OldData { get; set; }
public string Operation { get; set; }
}
}

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

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="2.11.23" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="NJsonSchema" Version="9.8.3" />
<PackageReference Include="NodaTime" Version="2.2.1" />
<PackageReference Include="RefactoringEssentials" Version="5.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

43
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs

@ -0,0 +1,43 @@
// ==========================================================================
// ContentExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public static class ContentValidationExtensions
{
public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList<ValidationError> errors)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidateAsync(data);
foreach (var error in validator.Errors)
{
errors.Add(error);
}
}
public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList<ValidationError> errors)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidatePartialAsync(data);
foreach (var error in validator.Errors)
{
errors.Add(error);
}
}
}
}

142
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -0,0 +1,142 @@
// ==========================================================================
// ContentValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
#pragma warning disable 168
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class ContentValidator
{
private readonly Schema schema;
private readonly PartitionResolver partitionResolver;
private readonly ValidationContext context;
private readonly ConcurrentBag<ValidationError> errors = new ConcurrentBag<ValidationError>();
public IReadOnlyCollection<ValidationError> Errors
{
get { return errors; }
}
public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context)
{
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(partitionResolver, nameof(partitionResolver));
this.schema = schema;
this.context = context;
this.partitionResolver = partitionResolver;
}
public Task ValidatePartialAsync(NamedContentData data)
{
Guard.NotNull(data, nameof(data));
var tasks = new List<Task>();
foreach (var fieldData in data)
{
var fieldName = fieldData.Key;
if (!schema.FieldsByName.TryGetValue(fieldData.Key, out var field))
{
errors.AddError("<FIELD> is not a known field.", fieldName);
}
else
{
tasks.Add(ValidateFieldPartialAsync(field, fieldData.Value));
}
}
return Task.WhenAll(tasks);
}
private Task ValidateFieldPartialAsync(Field field, ContentFieldData fieldData)
{
var partitioning = field.Partitioning;
var partition = partitionResolver(partitioning);
var tasks = new List<Task>();
foreach (var partitionValues in fieldData)
{
if (partition.TryGetItem(partitionValues.Key, out var item))
{
tasks.Add(field.ValidateAsync(partitionValues.Value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item)));
}
else
{
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field);
}
}
return Task.WhenAll(tasks);
}
public Task ValidateAsync(NamedContentData data)
{
Guard.NotNull(data, nameof(data));
ValidateUnknownFields(data);
var tasks = new List<Task>();
foreach (var field in schema.FieldsByName.Values)
{
var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData());
tasks.Add(ValidateFieldAsync(field, fieldData));
}
return Task.WhenAll(tasks);
}
private void ValidateUnknownFields(NamedContentData data)
{
foreach (var fieldData in data)
{
if (!schema.FieldsByName.ContainsKey(fieldData.Key))
{
errors.AddError("<FIELD> is not a known field.", fieldData.Key);
}
}
}
private Task ValidateFieldAsync(Field field, ContentFieldData fieldData)
{
var partitioning = field.Partitioning;
var partition = partitionResolver(partitioning);
var tasks = new List<Task>();
foreach (var partitionValues in fieldData)
{
if (!partition.TryGetItem(partitionValues.Key, out var _))
{
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field);
}
}
foreach (var item in partition)
{
var value = fieldData.GetOrCreate(item.Key, k => JValue.CreateNull());
tasks.Add(field.ValidateAsync(value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item)));
}
return Task.WhenAll(tasks);
}
}
}

58
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs

@ -0,0 +1,58 @@
// ==========================================================================
// FieldExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public static class FieldExtensions
{
public static void AddError(this ConcurrentBag<ValidationError> errors, string message, Field field, IFieldPartitionItem partitionItem = null)
{
AddError(errors, message, !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name, field.Name, partitionItem);
}
public static void AddError(this ConcurrentBag<ValidationError> errors, string message, string fieldName, IFieldPartitionItem partitionItem = null)
{
AddError(errors, message, fieldName, fieldName, partitionItem);
}
public static void AddError(this ConcurrentBag<ValidationError> errors, string message, string displayName, string fieldName, IFieldPartitionItem partitionItem = null)
{
if (partitionItem != null && partitionItem != InvariantPartitioning.Instance.Master)
{
displayName += $" ({partitionItem.Key})";
}
errors.Add(new ValidationError(message.Replace("<FIELD>", displayName), fieldName));
}
public static async Task ValidateAsync(this Field field, JToken value, ValidationContext context, Action<string> addError)
{
try
{
var typedValue = value.IsNull() ? null : JsonValueConverter.ConvertValue(field, value);
foreach (var validator in ValidatorsFactory.CreateValidators(field))
{
await validator.ValidateAsync(typedValue, context, addError);
}
}
catch
{
addError("<FIELD> is not a valid value.");
}
}
}
}

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

@ -0,0 +1,113 @@
// ==========================================================================
// JsonValueConverter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using NodaTime.Text;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class JsonValueConverter : IFieldVisitor<object>
{
public JToken Value { get; }
private JsonValueConverter(JToken value)
{
this.Value = value;
}
public static object ConvertValue(Field field, JToken json)
{
return field.Accept(new JsonValueConverter(json));
}
public object Visit(AssetsField field)
{
return Value.ToObject<List<Guid>>();
}
public object Visit(BooleanField field)
{
return (bool?)Value;
}
public object Visit(DateTimeField field)
{
if (Value.Type == JTokenType.String)
{
var parseResult = InstantPattern.General.Parse(Value.ToString());
if (!parseResult.Success)
{
throw parseResult.Exception;
}
return parseResult.Value;
}
throw new InvalidCastException("Invalid json type, expected string.");
}
public object Visit(GeolocationField field)
{
var geolocation = (JObject)Value;
foreach (var property in geolocation.Properties())
{
if (!string.Equals(property.Name, "latitude", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(property.Name, "longitude", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidCastException("Geolocation can only have latitude and longitude property.");
}
}
var lat = (double)geolocation["latitude"];
var lon = (double)geolocation["longitude"];
if (!lat.IsBetween(-90, 90))
{
throw new InvalidCastException("Latitude must be between -90 and 90.");
}
if (!lon.IsBetween(-180, 180))
{
throw new InvalidCastException("Longitude must be between -180 and 180.");
}
return Value;
}
public object Visit(JsonField field)
{
return Value;
}
public object Visit(NumberField field)
{
return (double?)Value;
}
public object Visit(ReferencesField field)
{
return Value.ToObject<List<Guid>>();
}
public object Visit(StringField field)
{
return Value.ToString();
}
public object Visit(TagsField field)
{
return Value.ToObject<List<string>>();
}
}
}

59
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs

@ -0,0 +1,59 @@
// ==========================================================================
// ValidationContext.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class ValidationContext
{
private readonly Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent;
private readonly Func<IEnumerable<Guid>, Task<IReadOnlyList<Guid>>> checkAsset;
public bool IsOptional { get; }
public ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent,
Func<IEnumerable<Guid>, Task<IReadOnlyList<Guid>>> checkAsset)
: this(checkContent, checkAsset, false)
{
}
private ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent,
Func<IEnumerable<Guid>, Task<IReadOnlyList<Guid>>> checkAsset,
bool isOptional)
{
Guard.NotNull(checkAsset, nameof(checkAsset));
Guard.NotNull(checkContent, nameof(checkAsset));
this.checkContent = checkContent;
this.checkAsset = checkAsset;
IsOptional = isOptional;
}
public ValidationContext Optional(bool isOptional)
{
return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, isOptional);
}
public Task<IReadOnlyList<Guid>> GetInvalidContentIdsAsync(IEnumerable<Guid> contentIds, Guid schemaId)
{
return checkContent(contentIds, schemaId);
}
public Task<IReadOnlyList<Guid>> GetInvalidAssetIdsAsync(IEnumerable<Guid> assetId)
{
return checkAsset(assetId);
}
}
}

45
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs

@ -0,0 +1,45 @@
// ==========================================================================
// AllowedValuesValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class AllowedValuesValidator<T> : IValidator
{
private readonly T[] allowedValues;
public AllowedValuesValidator(params T[] allowedValues)
{
Guard.NotNull(allowedValues, nameof(allowedValues));
this.allowedValues = allowedValues;
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
{
if (value == null)
{
return TaskHelper.Done;
}
var typedValue = (T)value;
if (!allowedValues.Contains(typedValue))
{
addError("<FIELD> is not an allowed value.");
}
return TaskHelper.Done;
}
}
}

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

@ -0,0 +1,30 @@
// ==========================================================================
// AssetsValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class AssetsValidator : IValidator
{
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
{
if (value is ICollection<Guid> assetIds)
{
var invalidIds = await context.GetInvalidAssetIdsAsync(assetIds);
foreach (var invalidId in invalidIds)
{
addError($"<FIELD> contains invalid asset '{invalidId}'.");
}
}
}
}
}

48
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs

@ -0,0 +1,48 @@
// ==========================================================================
// CollectionItemValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class CollectionItemValidator<T> : IValidator
{
private readonly IValidator[] itemValidators;
public CollectionItemValidator(params IValidator[] itemValidators)
{
Guard.NotNull(itemValidators, nameof(itemValidators));
Guard.NotEmpty(itemValidators, nameof(itemValidators));
this.itemValidators = itemValidators;
}
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
{
if (value is ICollection<T> items)
{
var innerContext = context.Optional(false);
var index = 1;
foreach (var item in items)
{
foreach (var itemValidator in itemValidators)
{
await itemValidator.ValidateAsync(item, innerContext, e => addError(e.Replace("<FIELD>", $"<FIELD> item #{index}")));
}
index++;
}
}
}
}
}

54
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs

@ -0,0 +1,54 @@
// ==========================================================================
// CollectionValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class CollectionValidator<T> : IValidator
{
private readonly bool isRequired;
private readonly int? minItems;
private readonly int? maxItems;
public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null)
{
this.isRequired = isRequired;
this.minItems = minItems;
this.maxItems = maxItems;
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
{
if (!(value is ICollection<T> items) || items.Count == 0)
{
if (isRequired && !context.IsOptional)
{
addError("<FIELD> is required.");
}
return TaskHelper.Done;
}
if (minItems.HasValue && items.Count < minItems.Value)
{
addError($"<FIELD> must have at least {minItems} item(s).");
}
if (maxItems.HasValue && items.Count > maxItems.Value)
{
addError($"<FIELD> must have not more than {maxItems} item(s).");
}
return TaskHelper.Done;
}
}
}

18
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs

@ -0,0 +1,18 @@
// ==========================================================================
// IValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public interface IValidator
{
Task ValidateAsync(object value, ValidationContext context, Action<string> addError);
}
}

48
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs

@ -0,0 +1,48 @@
// ==========================================================================
// PatternValidator.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public class PatternValidator : IValidator
{
private readonly Regex regex;
private readonly string errorMessage;
public PatternValidator(string pattern, string errorMessage = null)
{
this.errorMessage = errorMessage;
regex = new Regex("^" + pattern + "$");
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
{
if (value is string stringValue)
{
if (!string.IsNullOrEmpty(stringValue) && !regex.IsMatch(stringValue))
{
if (string.IsNullOrWhiteSpace(errorMessage))
{
addError("<FIELD> is not valid.");
}
else
{
addError(errorMessage);
}
}
}
return TaskHelper.Done;
}
}
}

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

Loading…
Cancel
Save