diff --git a/Squidex.sln b/Squidex.sln index 1c40c8524..fd8cc9914 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -65,6 +65,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution stylecop.json = stylecop.json EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Apps.Core.Model", "src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj", "{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 @@ -299,6 +303,30 @@ 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 @@ -328,6 +356,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} diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings index 322a6eebd..cf27c2d87 100644 --- a/Squidex.sln.DotSettings +++ b/Squidex.sln.DotSettings @@ -13,6 +13,10 @@ True True + + + + <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs new file mode 100644 index 000000000..fe4fa0bc5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// 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 AppClient(string name, string secret) + { + Guard.NotNullOrEmpty(name, nameof(name)); + Guard.NotNullOrEmpty(secret, nameof(secret)); + + this.name = name; + + this.secret = secret; + } + + public void Update(AppClientPermission newPermission) + { + Guard.Enum(newPermission, nameof(newPermission)); + + permission = newPermission; + } + + public void Rename(string newName) + { + Guard.NotNullOrEmpty(newName, nameof(newName)); + + name = newName; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs new file mode 100644 index 000000000..cbc4cdaf2 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs new file mode 100644 index 000000000..07001417f --- /dev/null +++ b/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 System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public class AppClients + { + private readonly Dictionary clients = new Dictionary(); + + public IReadOnlyDictionary Clients + { + get { return clients; } + } + + public void Add(string id, string secret) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + clients.Add(id, new AppClient(secret, id)); + } + + public void Revoke(string id) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + clients.Remove(id); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs new file mode 100644 index 000000000..c05d7527a --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs new file mode 100644 index 000000000..780ed8076 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// AppContributors.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public class AppContributors + { + private readonly Dictionary contributors = new Dictionary(); + + public IReadOnlyDictionary Contributors + { + get { return contributors; } + } + + public void Assign(string contributorId, AppContributorPermission permission) + { + Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); + Guard.Enum(permission, nameof(permission)); + + contributors[contributorId] = permission; + } + + public void Remove(string contributorId) + { + Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); + + contributors.Remove(contributorId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs new file mode 100644 index 000000000..e9bf142a6 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs new file mode 100644 index 000000000..40245615f --- /dev/null +++ b/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()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs new file mode 100644 index 000000000..81f11e599 --- /dev/null +++ b/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 : Dictionary, IEquatable> + { + public IEnumerable> ValidValues + { + get { return this.Where(x => x.Value != null); } + } + + protected ContentData(IEqualityComparer comparer) + : base(comparer) + { + } + + protected ContentData(IDictionary copy, IEqualityComparer comparer) + : base(copy, comparer) + { + } + + protected static TResult Merge(TResult source, TResult target) where TResult : ContentData + { + 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 source, TResult target) where TResult : ContentData + { + 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); + } + + public bool Equals(ContentData other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs new file mode 100644 index 000000000..cdf5f8464 --- /dev/null +++ b/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, IEquatable + { + 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.Default, JTokenEqualityComparer)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(EqualityComparer.Default, JTokenEqualityComparer); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs new file mode 100644 index 000000000..524e90f82 --- /dev/null +++ b/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, IEquatable + { + public IdContentData() + : base(EqualityComparer.Default) + { + } + + public IdContentData(IdContentData copy) + : base(copy, EqualityComparer.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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs new file mode 100644 index 000000000..71cc639f0 --- /dev/null +++ b/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, IEquatable + { + 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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs new file mode 100644 index 000000000..c2f84034d --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs new file mode 100644 index 000000000..30fbf23e1 --- /dev/null +++ b/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 Flow = new Dictionary + { + [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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs b/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs new file mode 100644 index 000000000..39a4434d2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// IFieldPartitionItem.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core +{ + public interface IFieldPartitionItem + { + string Key { get; } + + string Name { get; } + + bool IsOptional { get; } + + IEnumerable Fallback { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs b/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs new file mode 100644 index 000000000..ff4f97928 --- /dev/null +++ b/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 Master { get; } + + bool TryGetItem(string key, out IFieldPartitionItem item); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs new file mode 100644 index 000000000..5dd1ad0f8 --- /dev/null +++ b/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 IFieldPartitionItem.Fallback + { + get { return Enumerable.Empty(); } + } + + 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 IEnumerable.GetEnumerator() + { + yield return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs b/src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs new file mode 100644 index 000000000..63fd8904f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/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 +{ + 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 LanguageFallbacks + { + get { return languageFallbacks; } + } + + string IFieldPartitionItem.Key + { + get { return language.Iso2Code; } + } + + string IFieldPartitionItem.Name + { + get { return language.EnglishName; } + } + + IEnumerable IFieldPartitionItem.Fallback + { + get { return LanguageFallbacks.Select(x => x.Iso2Code); } + } + + public LanguageConfig(Language language, bool isOptional = false, IEnumerable 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]; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs new file mode 100644 index 000000000..c1d8fb2c8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs @@ -0,0 +1,172 @@ +// ========================================================================== +// 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; + +namespace Squidex.Domain.Apps.Core +{ + public sealed class LanguagesConfig : IFieldPartitioning + { + private State state; + + public LanguageConfig Master + { + get { return state.Master; } + } + + public int Count + { + get { return state.Languages.Count; } + } + + IFieldPartitionItem IFieldPartitioning.Master + { + get { return state.Master; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return state.Languages.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return state.Languages.Values.GetEnumerator(); + } + + private LanguagesConfig(ICollection 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)); + + state = new State( + 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), state.Master.Language == language ? null : state.Master); + } + + 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) + { + item = null; + + if (Language.IsValidLanguage(key) && state.Languages.TryGetValue(key, out var value)) + { + item = value; + + return true; + } + + return false; + } + + private sealed class State + { + public ImmutableDictionary Languages { get; } + + public LanguageConfig Master { get; } + + public State(ImmutableDictionary 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."); + } + + if (master.IsOptional) + { + throw new InvalidOperationException("Config has an optional master language."); + } + + this.Master = master; + } + } + + public PartitionResolver ToResolver() + { + return partitioning => + { + if (partitioning.Equals(Partitioning.Invariant)) + { + return InvariantPartitioning.Instance; + } + + return this; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs new file mode 100644 index 000000000..74f2ea770 --- /dev/null +++ b/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 + { + 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs new file mode 100644 index 000000000..3b9d9deae --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs new file mode 100644 index 000000000..cf1ad40a6 --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs new file mode 100644 index 000000000..70dc99448 --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs new file mode 100644 index 000000000..6be38b6f9 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs new file mode 100644 index 000000000..f773ce620 --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs new file mode 100644 index 000000000..0e71b0187 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs new file mode 100644 index 000000000..f3b1ba6fc --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs new file mode 100644 index 000000000..cb2be2f19 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs new file mode 100644 index 000000000..9c03a46c8 --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs new file mode 100644 index 000000000..2f9edf612 --- /dev/null +++ b/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(IFieldVisitor visitor); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs new file mode 100644 index 000000000..334d5b59e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// FieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json.Linq; + +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(IFieldPropertiesVisitor visitor); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs new file mode 100644 index 000000000..bf94a3a13 --- /dev/null +++ b/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 fieldsByPropertyType = new Dictionary(); + + 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( + (id, name, partitioning, properties) => + new BooleanField(id, name, partitioning, (BooleanFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new NumberField(id, name, partitioning, (NumberFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new StringField(id, name, partitioning, (StringFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new JsonField(id, name, partitioning, (JsonFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new AssetsField(id, name, partitioning, (AssetsFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new GeolocationField(id, name, partitioning, (GeolocationFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new ReferencesField(id, name, partitioning, (ReferencesFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new DateTimeField(id, name, partitioning, (DateTimeFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new TagsField(id, name, partitioning, (TagsFieldProperties)properties)); + + typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime"); + + typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References"); + } + + private void Add(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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs new file mode 100644 index 000000000..edc05dbb2 --- /dev/null +++ b/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 : 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs new file mode 100644 index 000000000..fc5b313a0 --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs new file mode 100644 index 000000000..d6f3acff7 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs new file mode 100644 index 000000000..5e4b34d4e --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs new file mode 100644 index 000000000..97087296a --- /dev/null +++ b/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 + { + 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); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs new file mode 100644 index 000000000..eecd20a73 --- /dev/null +++ b/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 + { + 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); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs new file mode 100644 index 000000000..51b103012 --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs new file mode 100644 index 000000000..f2c6695bd --- /dev/null +++ b/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 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; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs new file mode 100644 index 000000000..5502fd3f9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// SchemaConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class SchemaConverter : JsonConverter + { + private readonly FieldRegistry fieldRegistry; + + public SchemaConverter(FieldRegistry fieldRegistry) + { + Guard.NotNull(fieldRegistry, nameof(fieldRegistry)); + + this.fieldRegistry = fieldRegistry; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, new JsonSchemaModel((Schema)value)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader).ToSchema(fieldRegistry); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Schema); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs new file mode 100644 index 000000000..49fab3979 --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs new file mode 100644 index 000000000..c432be09a --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs new file mode 100644 index 000000000..dfcbba099 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// NamedElementPropertiesBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class NamedElementPropertiesBase + { + public string Label { get; set; } + + public string Hints { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs new file mode 100644 index 000000000..d19b05c39 --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs new file mode 100644 index 000000000..28f164ef8 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs new file mode 100644 index 000000000..ce1703faa --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs new file mode 100644 index 000000000..f2dc2bc5b --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs new file mode 100644 index 000000000..8a4f76192 --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs new file mode 100644 index 000000000..8a7a06d40 --- /dev/null +++ b/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 fieldsOrdered = new List(); + private readonly Dictionary fieldsById = new Dictionary(); + private readonly Dictionary fieldsByName = new Dictionary(); + private SchemaProperties properties = new SchemaProperties(); + private bool isPublished; + + public string Name + { + get { return name; } + } + + public bool IsPublished + { + get { return isPublished; } + } + + public IReadOnlyList Fields + { + get { return fieldsOrdered; } + } + + public IReadOnlyDictionary FieldsById + { + get { return fieldsById; } + } + + public IReadOnlyDictionary 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 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); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs new file mode 100644 index 000000000..4731958e7 --- /dev/null +++ b/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 + { + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs new file mode 100644 index 000000000..d2587fd79 --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs new file mode 100644 index 000000000..1226fef19 --- /dev/null +++ b/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 + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs new file mode 100644 index 000000000..5263cc2e7 --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs new file mode 100644 index 000000000..a36d061f1 --- /dev/null +++ b/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 + { + 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(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs new file mode 100644 index 000000000..9e10e634b --- /dev/null +++ b/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(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj new file mode 100644 index 000000000..754774561 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + Squidex.Domain.Apps.Core + + + full + True + + + + + + + + + + ..\..\Squidex.ruleset + + diff --git a/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs new file mode 100644 index 000000000..169e85b7d --- /dev/null +++ b/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; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs new file mode 100644 index 000000000..30edad325 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs @@ -0,0 +1,201 @@ +// ========================================================================== +// 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.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 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(); + + 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs new file mode 100644 index 000000000..10f73cb5e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// ContentEnricher.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +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)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs new file mode 100644 index 000000000..68a5e044d --- /dev/null +++ b/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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs new file mode 100644 index 000000000..d7529af47 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// ValidatorsFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +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 + { + 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(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs new file mode 100644 index 000000000..d5813b87f --- /dev/null +++ b/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 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 GetReferencedIds(this IdContentData source, Schema schema) + { + Guard.NotNull(schema, nameof(schema)); + + var foundReferences = new HashSet(); + + 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; + } + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs new file mode 100644 index 000000000..31f67410f --- /dev/null +++ b/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 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 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 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs new file mode 100644 index 000000000..f70c8b9f0 --- /dev/null +++ b/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 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(); + } + + public static IEnumerable Visit(AssetsField field, JToken value) + { + IEnumerable result = null; + try + { + result = value?.ToObject>(); + } + catch + { + result = null; + } + + return result ?? Enumerable.Empty(); + } + + private static IEnumerable Visit(ReferencesField field, JToken value) + { + IEnumerable result = null; + try + { + result = value?.ToObject>() ?? Enumerable.Empty(); + } + catch + { + result = Enumerable.Empty(); + } + + return result.Union(new[] { field.Properties.SchemaId }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs new file mode 100644 index 000000000..767f89839 --- /dev/null +++ b/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 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs new file mode 100644 index 000000000..c80d94fc1 --- /dev/null +++ b/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 + { + 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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs new file mode 100644 index 000000000..9ba399359 --- /dev/null +++ b/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 }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs new file mode 100644 index 000000000..d20a28607 --- /dev/null +++ b/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 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; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs new file mode 100644 index 000000000..a2c3b6f7d --- /dev/null +++ b/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 + { + private readonly Func schemaResolver; + + public JsonTypeVisitor(Func 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(); + + 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 updater) + { + var property = new JsonProperty { IsRequired = field.RawProperties.IsRequired }; + + updater(property); + + return property; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs new file mode 100644 index 000000000..8ceadc1ba --- /dev/null +++ b/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 fieldsToDelete; + private Dictionary 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(); + } + + 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> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + foreach (var property in fieldProperties) + { + yield return new KeyValuePair(property.Key, property.Value); + } + } + + private void EnsurePropertiesInitialized() + { + if (fieldProperties == null) + { + fieldProperties = new Dictionary(contentData.Count); + + foreach (var kvp in contentData) + { + fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs new file mode 100644 index 000000000..6542d772b --- /dev/null +++ b/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); + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs new file mode 100644 index 000000000..e4eed7641 --- /dev/null +++ b/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 valuesToDelete; + private Dictionary 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(); + } + + 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> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + foreach (var property in valueProperties) + { + yield return new KeyValuePair(property.Key, property.Value); + } + } + + private void EnsurePropertiesInitialized() + { + if (valueProperties == null) + { + valueProperties = new Dictionary(FieldData.Count); + + foreach (var kvp in FieldData) + { + valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs new file mode 100644 index 000000000..01e50da58 --- /dev/null +++ b/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; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs new file mode 100644 index 000000000..6704c5440 --- /dev/null +++ b/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)); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs new file mode 100644 index 000000000..bad0351f6 --- /dev/null +++ b/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); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs new file mode 100644 index 000000000..6ab62dc73 --- /dev/null +++ b/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(message => + { + var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; + + throw new DomainForbiddenException(exMessage); + })); + } + + private static void EnableReject(Engine engine) + { + engine.SetValue("reject", new Action(message => + { + var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; + + throw new ValidationException($"Script rejected the operation.", errors); + })); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs new file mode 100644 index 000000000..8974099d1 --- /dev/null +++ b/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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs new file mode 100644 index 000000000..fcc5a0733 --- /dev/null +++ b/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; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj new file mode 100644 index 000000000..ec9a21f4c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -0,0 +1,28 @@ + + + netstandard2.0 + Squidex.Domain.Apps.Core + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs new file mode 100644 index 000000000..e08089efc --- /dev/null +++ b/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 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 errors) + { + var validator = new ContentValidator(schema, partitionResolver, context); + + await validator.ValidatePartialAsync(data); + + foreach (var error in validator.Errors) + { + errors.Add(error); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs new file mode 100644 index 000000000..d9470234f --- /dev/null +++ b/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 errors = new ConcurrentBag(); + + public IReadOnlyCollection 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(); + + foreach (var fieldData in data) + { + var fieldName = fieldData.Key; + + if (!schema.FieldsByName.TryGetValue(fieldData.Key, out var field)) + { + errors.AddError(" 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(); + + 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($" 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(); + + 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(" 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(); + + foreach (var partitionValues in fieldData) + { + if (!partition.TryGetItem(partitionValues.Key, out var _)) + { + errors.AddError($" 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); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs new file mode 100644 index 000000000..b56bf1908 --- /dev/null +++ b/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 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 errors, string message, string fieldName, IFieldPartitionItem partitionItem = null) + { + AddError(errors, message, fieldName, fieldName, partitionItem); + } + + public static void AddError(this ConcurrentBag 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("", displayName), fieldName)); + } + + public static async Task ValidateAsync(this Field field, JToken value, ValidationContext context, Action 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(" is not a valid value."); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs new file mode 100644 index 000000000..00f4f3e9d --- /dev/null +++ b/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 + { + 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>(); + } + + 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>(); + } + + public object Visit(StringField field) + { + return Value.ToString(); + } + + public object Visit(TagsField field) + { + return Value.ToObject>(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs new file mode 100644 index 000000000..2d6c9925d --- /dev/null +++ b/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, Guid, Task>> checkContent; + private readonly Func, Task>> checkAsset; + + public bool IsOptional { get; } + + public ValidationContext( + Func, Guid, Task>> checkContent, + Func, Task>> checkAsset) + : this(checkContent, checkAsset, false) + { + } + + private ValidationContext( + Func, Guid, Task>> checkContent, + Func, Task>> 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> GetInvalidContentIdsAsync(IEnumerable contentIds, Guid schemaId) + { + return checkContent(contentIds, schemaId); + } + + public Task> GetInvalidAssetIdsAsync(IEnumerable assetId) + { + return checkAsset(assetId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs new file mode 100644 index 000000000..e5fd7706b --- /dev/null +++ b/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 : 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 addError) + { + if (value == null) + { + return TaskHelper.Done; + } + + var typedValue = (T)value; + + if (!allowedValues.Contains(typedValue)) + { + addError(" is not an allowed value."); + } + + return TaskHelper.Done; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs new file mode 100644 index 000000000..e7c504577 --- /dev/null +++ b/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 addError) + { + if (value is ICollection assetIds) + { + var invalidIds = await context.GetInvalidAssetIdsAsync(assetIds); + + foreach (var invalidId in invalidIds) + { + addError($" contains invalid asset '{invalidId}'."); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs new file mode 100644 index 000000000..750acad7c --- /dev/null +++ b/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 : 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 addError) + { + if (value is ICollection 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("", $" item #{index}"))); + } + + index++; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs new file mode 100644 index 000000000..6f24890c2 --- /dev/null +++ b/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 : 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 addError) + { + if (!(value is ICollection items) || items.Count == 0) + { + if (isRequired && !context.IsOptional) + { + addError(" is required."); + } + + return TaskHelper.Done; + } + + if (minItems.HasValue && items.Count < minItems.Value) + { + addError($" must have at least {minItems} item(s)."); + } + + if (maxItems.HasValue && items.Count > maxItems.Value) + { + addError($" must have not more than {maxItems} item(s)."); + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs new file mode 100644 index 000000000..597d01d09 --- /dev/null +++ b/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 addError); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs new file mode 100644 index 000000000..927156734 --- /dev/null +++ b/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 addError) + { + if (value is string stringValue) + { + if (!string.IsNullOrEmpty(stringValue) && !regex.IsMatch(stringValue)) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + { + addError(" is not valid."); + } + else + { + addError(errorMessage); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs new file mode 100644 index 000000000..141286630 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// RangeValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class RangeValidator : IValidator where T : struct, IComparable + { + private readonly T? min; + private readonly T? max; + + public RangeValidator(T? min, T? max) + { + if (min.HasValue && max.HasValue && min.Value.CompareTo(max.Value) >= 0) + { + throw new ArgumentException("Min value must be greater than max value.", nameof(min)); + } + + this.min = min; + this.max = max; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value == null) + { + return TaskHelper.Done; + } + + var typedValue = (T)value; + + if (min.HasValue && typedValue.CompareTo(min.Value) < 0) + { + addError($" must be greater or equals than '{min}'."); + } + + if (max.HasValue && typedValue.CompareTo(max.Value) > 0) + { + addError($" must be less or equals than '{max}'."); + } + + return TaskHelper.Done; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs new file mode 100644 index 000000000..081e54835 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// ReferencesValidator.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 ReferencesValidator : IValidator + { + private readonly Guid schemaId; + + public ReferencesValidator(Guid schemaId) + { + this.schemaId = schemaId; + } + + public async Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value is ICollection contentIds) + { + var invalidIds = await context.GetInvalidContentIdsAsync(contentIds, schemaId); + + foreach (var invalidId in invalidIds) + { + addError($" contains invalid reference '{invalidId}'."); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs new file mode 100644 index 000000000..67f118e49 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// RequiredStringValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredStringValidator : IValidator + { + private readonly bool validateEmptyStrings; + + public RequiredStringValidator(bool validateEmptyStrings = false) + { + this.validateEmptyStrings = validateEmptyStrings; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (context.IsOptional || (value != null && !(value is string))) + { + return TaskHelper.Done; + } + + var valueAsString = (string)value; + + if (valueAsString == null || (validateEmptyStrings && string.IsNullOrWhiteSpace(valueAsString))) + { + addError(" is required."); + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs new file mode 100644 index 000000000..2c5f51269 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// RequiredValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredValidator : IValidator + { + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value == null && !context.IsOptional) + { + addError(" is required."); + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs new file mode 100644 index 000000000..0a742ba49 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// StringLengthValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class StringLengthValidator : IValidator + { + private readonly int? minLength; + private readonly int? maxLength; + + public StringLengthValidator(int? minLength, int? maxLength) + { + if (minLength.HasValue && maxLength.HasValue && minLength.Value >= maxLength.Value) + { + throw new ArgumentException("Min length must be greater than max length.", nameof(minLength)); + } + + this.minLength = minLength; + this.maxLength = maxLength; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + if (minLength.HasValue && stringValue.Length < minLength.Value) + { + addError($" must have more than '{minLength}' characters."); + } + + if (maxLength.HasValue && stringValue.Length > maxLength.Value) + { + addError($" must have less than '{maxLength}' characters."); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs new file mode 100644 index 000000000..735b079ad --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs @@ -0,0 +1,145 @@ +// ========================================================================== +// ValidatorsFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class ValidatorsFactory : IFieldPropertiesVisitor> + { + private static readonly ValidatorsFactory Instance = new ValidatorsFactory(); + + private ValidatorsFactory() + { + } + + public static IEnumerable CreateValidators(Field field) + { + Guard.NotNull(field, nameof(field)); + + return field.RawProperties.Accept(Instance); + } + + public IEnumerable Visit(AssetsFieldProperties properties) + { + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + } + + yield return new AssetsValidator(); + } + + public IEnumerable Visit(BooleanFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(DateTimeFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (properties.MinValue.HasValue || properties.MaxValue.HasValue) + { + yield return new RangeValidator(properties.MinValue, properties.MaxValue); + } + } + + public IEnumerable Visit(GeolocationFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(JsonFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(NumberFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (properties.MinValue.HasValue || properties.MaxValue.HasValue) + { + yield return new RangeValidator(properties.MinValue, properties.MaxValue); + } + + if (properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(properties.AllowedValues.ToArray()); + } + } + + public IEnumerable Visit(ReferencesFieldProperties properties) + { + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + } + + if (properties.SchemaId != Guid.Empty) + { + yield return new ReferencesValidator(properties.SchemaId); + } + } + + public IEnumerable Visit(StringFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredStringValidator(); + } + + if (properties.MinLength.HasValue || properties.MaxLength.HasValue) + { + yield return new StringLengthValidator(properties.MinLength, properties.MaxLength); + } + + if (!string.IsNullOrWhiteSpace(properties.Pattern)) + { + yield return new PatternValidator(properties.Pattern, properties.PatternMessage); + } + + if (properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(properties.AllowedValues.ToArray()); + } + } + + public IEnumerable Visit(TagsFieldProperties properties) + { + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + } + + yield return new CollectionItemValidator(new RequiredStringValidator()); + } + } +} diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index 72df850d5..9a3b56d23 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -26,7 +26,7 @@ if ($all -Or $infrastructure) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Infrastructure*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Infrastructure.xml" ` -oldStyle @@ -37,7 +37,7 @@ if ($all -Or $appsCore) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Apps.Core*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Core.xml" ` -oldStyle @@ -48,7 +48,7 @@ if ($all -Or $appsRead) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Apps.Read.Tests\Squidex.Domain.Apps.Read.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Apps.Read*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Read.xml" ` -oldStyle @@ -59,7 +59,7 @@ if ($all -Or $appsWrite) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Apps.Write.Tests\Squidex.Domain.Apps.Write.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Apps.Write*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Write.xml" ` -oldStyle @@ -70,7 +70,7 @@ if ($all -Or $users) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Users.Tests\Squidex.Domain.Users.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Users*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Users.xml" ` -oldStyle diff --git a/tests/Squidex.Domain.Apps.Core.Tests/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/ContentEnrichmentTests.cs deleted file mode 100644 index 6a147aeb1..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/ContentEnrichmentTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// ContentEnrichmentTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; -using NodaTime.Text; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core -{ - public class ContentEnrichmentTests - { - private static readonly Instant Now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - private readonly Schema schema = - Schema.Create("my-schema", new SchemaProperties()) - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())) - .AddField(new StringField(2, "my-string", Partitioning.Language, - new StringFieldProperties { DefaultValue = "en-string" })) - .AddField(new NumberField(3, "my-number", Partitioning.Invariant, - new NumberFieldProperties { DefaultValue = 123 })) - .AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties { DefaultValue = true })) - .AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = Now })) - .AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) - .AddField(new GeolocationField(8, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())); - - [Fact] - private void Should_enrich_with_default_values() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "de-string")) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal(456, (int)data["my-number"]["iv"]); - - Assert.Equal("de-string", (string)data["my-string"]["de"]); - Assert.Equal("en-string", (string)data["my-string"]["en"]); - - Assert.Equal(Now, InstantPattern.General.Parse((string)data["my-datetime"]["iv"]).Value); - - Assert.True((bool)data["my-boolean"]["iv"]); - } - - [Fact] - private void Should_also_enrich_with_default_values_when_string_is_empty() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", string.Empty)) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal("en-string", (string)data["my-string"]["de"]); - Assert.Equal("en-string", (string)data["my-string"]["en"]); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Apps/RoleExtensionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleExtensionTests.cs similarity index 95% rename from tests/Squidex.Domain.Apps.Core.Tests/Apps/RoleExtensionTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleExtensionTests.cs index 743d541a6..89927742e 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Apps/RoleExtensionTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleExtensionTests.cs @@ -7,9 +7,10 @@ // ========================================================================== using System; +using Squidex.Domain.Apps.Core.Apps; using Xunit; -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps { public class RoleExtensionTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs new file mode 100644 index 000000000..cb4b36c77 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs @@ -0,0 +1,208 @@ +// ========================================================================== +// ContentDataTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Model.Contents +{ + public class ContentDataTests + { + [Fact] + public void Should_remove_null_values_from_name_model_when_cleaning() + { + var input = + new NamedContentData() + .AddField("field1", null) + .AddField("field2", + new ContentFieldData() + .AddValue("en", 2) + .AddValue("it", null)); + + var actual = input.ToCleaned(); + + var expected = + new NamedContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("en", 2)); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_remove_null_values_from_id_model_when_cleaning() + { + var input = + new IdContentData() + .AddField(1, null) + .AddField(2, + new ContentFieldData() + .AddValue("en", 2) + .AddValue("it", null)); + + var actual = input.ToCleaned(); + + var expected = + new IdContentData() + .AddField(2, + new ContentFieldData() + .AddValue("en", 2)); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_merge_two_name_models() + { + var lhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", 2)); + + var rhs = + new NamedContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("en", 3)) + .AddField("field3", + new ContentFieldData() + .AddValue("iv", 4)); + + var expected = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", 2) + .AddValue("en", 3)) + .AddField("field3", + new ContentFieldData() + .AddValue("iv", 4)); + + var actual = lhs.MergeInto(rhs); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_merge_two_id_models() + { + var lhs = + new IdContentData() + .AddField(1, + new ContentFieldData() + .AddValue("iv", 1)) + .AddField(2, + new ContentFieldData() + .AddValue("de", 2)); + + var rhs = + new IdContentData() + .AddField(2, + new ContentFieldData() + .AddValue("en", 3)) + .AddField(3, + new ContentFieldData() + .AddValue("iv", 4)); + + var expected = + new IdContentData() + .AddField(1, + new ContentFieldData() + .AddValue("iv", 1)) + .AddField(2, + new ContentFieldData() + .AddValue("de", 2) + .AddValue("en", 3)) + .AddField(3, + new ContentFieldData() + .AddValue("iv", 4)); + + var actual = lhs.MergeInto(rhs); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_be_equal_when_data_have_same_structure() + { + var lhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + var rhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + Assert.True(lhs.Equals(rhs)); + Assert.True(lhs.Equals((object)rhs)); + Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); + } + + [Fact] + public void Should_not_be_equal_when_data_have_not_same_structure() + { + var lhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + var rhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("en", 2)) + .AddField("field3", + new ContentFieldData() + .AddValue("iv", 2)); + + Assert.False(lhs.Equals(rhs)); + Assert.False(lhs.Equals((object)rhs)); + Assert.NotEqual(lhs.GetHashCode(), rhs.GetHashCode()); + } + + [Fact] + public void Should_be_equal_fields_when_they_have_same_value() + { + var lhs = + new ContentFieldData() + .AddValue("iv", 2); + + var rhs = + new ContentFieldData() + .AddValue("iv", 2); + + Assert.True(lhs.Equals(rhs)); + Assert.True(lhs.Equals((object)rhs)); + Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs index bf7060ff4..5600a3122 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs @@ -6,9 +6,10 @@ // All rights reserved. // ========================================================================== +using Squidex.Domain.Apps.Core.Contents; using Xunit; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents { public class StatusFlowTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/InvariantPartitionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs similarity index 97% rename from tests/Squidex.Domain.Apps.Core.Tests/InvariantPartitionTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs index a39b6f13c..0a5c4f8e7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/InvariantPartitionTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs @@ -13,7 +13,7 @@ using Xunit; #pragma warning disable xUnit2013 // Do not use equality check to check for collection size. -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Model { public sealed class InvariantPartitionTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/LanguagesConfigTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs similarity index 58% rename from tests/Squidex.Domain.Apps.Core.Tests/LanguagesConfigTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs index 7584f61ab..d801a9810 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/LanguagesConfigTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -13,14 +14,14 @@ using FluentAssertions; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Model { public class LanguagesConfigTests { [Fact] public void Should_create_initial_config() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -36,7 +37,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_create_initial_config_with_multiple_languages() { - var config = LanguagesConfig.Create(Language.DE, Language.EN, Language.IT); + var config = LanguagesConfig.Build(Language.DE, Language.EN, Language.IT); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -59,7 +60,7 @@ namespace Squidex.Domain.Apps.Core new LanguageConfig(Language.EN), new LanguageConfig(Language.IT) }; - var config = LanguagesConfig.Create(configs); + var config = LanguagesConfig.Build(configs); config.OfType().ToList().ShouldBeEquivalentTo(configs); @@ -69,7 +70,9 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_add_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT); + var config = LanguagesConfig.Build(Language.DE); + + config.Set(new LanguageConfig(Language.IT)); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -77,28 +80,35 @@ namespace Squidex.Domain.Apps.Core new LanguageConfig(Language.DE), new LanguageConfig(Language.IT) }); + + Assert.True(config.TryGetConfig(Language.IT, out var _)); + Assert.True(config.Contains(Language.IT)); } [Fact] public void Should_make_first_language_to_master() { - var config = LanguagesConfig.Empty.Add(Language.IT); + var config = LanguagesConfig.Build(Language.IT); Assert.Equal(Language.IT, config.Master.Language); } [Fact] - public void Should_throw_exception_if_language_to_add_already_exists() + public void Should_not_throw_exception_if_language_to_add_already_exists() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Add(Language.DE)); + config.Set(new LanguageConfig(Language.DE)); } [Fact] public void Should_make_master_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).MakeMaster(Language.IT); + var config = LanguagesConfig.Build(Language.DE); + + config.Set(new LanguageConfig(Language.UK)); + config.Set(new LanguageConfig(Language.IT)); + config.MakeMaster(Language.IT); Assert.Equal(Language.IT, config.Master.Language); } @@ -106,15 +116,15 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_throw_exception_if_language_to_make_master_is_not_found() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.MakeMaster(Language.EN)); + Assert.Throws(() => config.MakeMaster(Language.EN)); } [Fact] public void Should_not_throw_exception_if_language_is_already_master_language() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); config.MakeMaster(Language.DE); } @@ -122,7 +132,9 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_remove_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Add(Language.RU).Remove(Language.IT); + var config = LanguagesConfig.Build(Language.DE, Language.IT, Language.RU); + + config.Remove(Language.IT); config.ToList().ShouldBeEquivalentTo( new List @@ -136,41 +148,44 @@ namespace Squidex.Domain.Apps.Core public void Should_remove_fallbacks_when_removing_language() { var config = - LanguagesConfig.Create(Language.DE) - .Add(Language.IT) - .Add(Language.RU) - .Update(Language.DE, false, false, new[] { Language.RU, Language.IT }) - .Update(Language.RU, false, false, new[] { Language.DE, Language.IT }) - .Remove(Language.IT); + LanguagesConfig.Build( + new LanguageConfig(Language.DE), + new LanguageConfig(Language.IT, false, Language.RU, Language.IT), + new LanguageConfig(Language.RU, false, Language.DE, Language.IT)); + + config.Remove(Language.IT); config.OfType().ToList().ShouldBeEquivalentTo( new List { - new LanguageConfig(Language.DE, false, Language.RU), + new LanguageConfig(Language.DE), new LanguageConfig(Language.RU, false, Language.DE) }); } [Fact] - public void Should_throw_exception_if_language_to_remove_is_not_found() + public void Should_not_throw_exception_if_language_to_remove_is_not_found() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Remove(Language.EN)); + config.Remove(Language.EN); } [Fact] public void Should_throw_exception_if_language_to_remove_is_master_language() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Remove(Language.DE)); + Assert.Throws(() => config.Remove(Language.DE)); } [Fact] public void Should_update_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, false, new[] { Language.DE }); + var config = LanguagesConfig.Build(Language.DE); + + config.Set(new LanguageConfig(Language.IT)); + config.Set(new LanguageConfig(Language.IT, true, Language.DE)); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -180,52 +195,28 @@ namespace Squidex.Domain.Apps.Core }); } - [Fact] - public void Should_also_set_make_master_when_updating_language() - { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, false, true, null); - - Assert.Equal(Language.IT, config.Master.Language); - } - - [Fact] - public void Should_throw_exception_if_language_to_update_is_not_found() - { - var config = LanguagesConfig.Create(Language.DE); - - Assert.Throws(() => config.Update(Language.EN, true, false, null)); - } - [Fact] public void Should_throw_exception_if_fallback_language_is_invalid() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Update(Language.DE, false, false, new[] { Language.EN })); + Assert.Throws(() => config.Set(new LanguageConfig(Language.DE, false, Language.EN))); } [Fact] public void Should_throw_exception_if_language_to_make_optional_is_master_language() { - var config = LanguagesConfig.Create(Language.DE); - - Assert.Throws(() => config.Update(Language.DE, true, false, null)); - } - - [Fact] - public void Should_throw_exception_if_language_to_make_optional_must_be_set_to_master() - { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Update(Language.DE, true, true, null)); + Assert.Throws(() => config.Set(new LanguageConfig(Language.DE, true))); } [Fact] public void Should_provide_enumerators() { - var config = LanguagesConfig.Create(); + var config = LanguagesConfig.Build(Language.DE); - Assert.Empty(config); + Assert.NotEmpty(config); Assert.NotNull(((IEnumerable)config).GetEnumerator()); Assert.NotNull(((IEnumerable)config).GetEnumerator()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/PartitioningTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Core.Tests/PartitioningTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs index 41fbb3c00..10ed1df6f 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/PartitioningTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs @@ -8,7 +8,7 @@ using Xunit; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Model { public sealed class PartitioningTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs new file mode 100644 index 000000000..8195d2beb --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// FieldRegistryTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class FieldRegistryTests + { + private readonly FieldRegistry sut = new FieldRegistry(new TypeNameRegistry()); + + private sealed class InvalidProperties : FieldProperties + { + public override T Accept(IFieldPropertiesVisitor visitor) + { + return default(T); + } + } + + [Fact] + public void Should_throw_exception_if_creating_field_and_field_is_not_registered() + { + Assert.Throws(() => sut.CreateField(1, "name", Partitioning.Invariant, new InvalidProperties())); + } + + [Theory] + [InlineData( + typeof(AssetsFieldProperties), + typeof(AssetsField))] + [InlineData( + typeof(BooleanFieldProperties), + typeof(BooleanField))] + [InlineData( + typeof(DateTimeFieldProperties), + typeof(DateTimeField))] + [InlineData( + typeof(GeolocationFieldProperties), + typeof(GeolocationField))] + [InlineData( + typeof(JsonFieldProperties), + typeof(JsonField))] + [InlineData( + typeof(NumberFieldProperties), + typeof(NumberField))] + [InlineData( + typeof(ReferencesFieldProperties), + typeof(ReferencesField))] + [InlineData( + typeof(StringFieldProperties), + typeof(StringField))] + [InlineData( + typeof(TagsFieldProperties), + typeof(TagsField))] + public void Should_create_field_by_properties(Type propertyType, Type fieldType) + { + var properties = (FieldProperties)Activator.CreateInstance(propertyType); + + var field = sut.CreateField(1, "name", Partitioning.Invariant, properties); + + Assert.Equal(properties, field.RawProperties); + Assert.Equal(fieldType, field.GetType()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs new file mode 100644 index 000000000..941c85d5e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// JsonSerializerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas.Json +{ + public class JsonSerializerTests + { + private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); + private readonly JsonSerializer serializer; + private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + + public JsonSerializerTests() + { + serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); + + serializerSettings.ContractResolver = new ConverterContractResolver( + new InstantConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new RefTokenConverter(), + new SchemaConverter(new FieldRegistry(typeNameRegistry)), + new StringEnumConverter()); + + serializerSettings.TypeNameHandling = TypeNameHandling.Auto; + + serializer = JsonSerializer.Create(serializerSettings); + } + + [Fact] + public void Should_serialize_and_deserialize_schema() + { + var schemaSource = TestData.MixedSchema(); + var schemaTarget = JToken.FromObject(schemaSource, serializer).ToObject(serializer); + + schemaTarget.ShouldBeEquivalentTo(schemaSource); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs new file mode 100644 index 000000000..8d5fbebfa --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -0,0 +1,217 @@ +// ========================================================================== +// SchemaTests.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.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class SchemaTests + { + private readonly Schema sut = new Schema("my-schema"); + + [Fact] + public void Should_instantiate_schema() + { + Assert.Equal("my-schema", sut.Name); + } + + [Fact] + public void Should_throw_exception_if_creating_schema_with_invalid_name() + { + Assert.Throws(() => new Schema(string.Empty)); + } + + [Fact] + public void Should_update_schema() + { + var properties = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; + + sut.Update(properties); + + Assert.Equal(properties, sut.Properties); + } + + [Fact] + public void Should_add_field() + { + var field = AddNumberField(1); + + Assert.Equal(field, sut.FieldsById[1]); + } + + [Fact] + public void Should_throw_exception_if_adding_field_with_name_that_already_exists() + { + AddNumberField(1); + + Assert.Throws(() => sut.AddField(new NumberField(2, "my-field-1", Partitioning.Invariant))); + } + + [Fact] + public void Should_throw_exception_if_adding_field_with_id_that_already_exists() + { + AddNumberField(1); + + Assert.Throws(() => sut.AddField(new NumberField(1, "my-field-2", Partitioning.Invariant))); + } + + [Fact] + public void Should_hide_field() + { + AddNumberField(1); + + sut.FieldsById[1].Hide(); + sut.FieldsById[1].Hide(); + + Assert.True(sut.FieldsById[1].IsHidden); + } + + [Fact] + public void Should_show_field() + { + AddNumberField(1); + + sut.FieldsById[1].Hide(); + sut.FieldsById[1].Show(); + sut.FieldsById[1].Show(); + + Assert.False(sut.FieldsById[1].IsHidden); + } + + [Fact] + public void Should_disable_field() + { + AddNumberField(1); + + sut.FieldsById[1].Disable(); + sut.FieldsById[1].Disable(); + + Assert.True(sut.FieldsById[1].IsDisabled); + } + + [Fact] + public void Should_enable_field() + { + AddNumberField(1); + + sut.FieldsById[1].Disable(); + sut.FieldsById[1].Enable(); + sut.FieldsById[1].Enable(); + + Assert.False(sut.FieldsById[1].IsDisabled); + } + + [Fact] + public void Should_lock_field() + { + AddNumberField(1); + + sut.FieldsById[1].Lock(); + + Assert.True(sut.FieldsById[1].IsLocked); + } + + [Fact] + public void Should_do_nothing_if_field_to_delete_not_found() + { + AddNumberField(1); + + sut.DeleteField(2); + + Assert.Equal(1, sut.FieldsById.Count); + } + + [Fact] + public void Should_delete_field() + { + AddNumberField(1); + + sut.DeleteField(1); + + Assert.Empty(sut.FieldsById); + } + + [Fact] + public void Should_update_field() + { + AddNumberField(1); + + sut.FieldsById[1].Update(new NumberFieldProperties { Hints = "my-hints" }); + + Assert.Equal("my-hints", sut.FieldsById[1].RawProperties.Hints); + } + + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() + { + AddNumberField(1); + + Assert.Throws(() => sut.FieldsById[1].Update(new StringFieldProperties())); + } + + [Fact] + public void Should_publish_schema() + { + sut.Publish(); + + Assert.True(sut.IsPublished); + } + + [Fact] + public void Should_unpublish_schema() + { + sut.Publish(); + sut.Unpublish(); + + Assert.False(sut.IsPublished); + } + + [Fact] + public void Should_reorder_fields() + { + var field1 = AddNumberField(1); + var field2 = AddNumberField(2); + var field3 = AddNumberField(3); + + sut.ReorderFields(new List { 3, 2, 1 }); + + Assert.Equal(new List { field3, field2, field1 }, sut.Fields.ToList()); + } + + [Fact] + public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() + { + AddNumberField(1); + AddNumberField(2); + + Assert.Throws(() => sut.ReorderFields(new List { 1 })); + } + + [Fact] + public void Should_throw_exception_if_field_to_reorder_does_not_exist() + { + AddNumberField(1); + AddNumberField(2); + + Assert.Throws(() => sut.ReorderFields(new List { 1, 4 })); + } + + private NumberField AddNumberField(int id) + { + var field = new NumberField(id, $"my-field-{id}", Partitioning.Invariant); + + sut.AddField(field); + + return field; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Contents/ContentDataTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs similarity index 66% rename from tests/Squidex.Domain.Apps.Core.Tests/Contents/ContentDataTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs index 0824c91d3..a6c436d1a 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Contents/ContentDataTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs @@ -8,27 +8,36 @@ using System; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Xunit; #pragma warning disable xUnit2013 // Do not use equality check to check for collection size. -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent { - public class ContentDataTests + public class ContentConversionTests { - private readonly Schema schema = - Schema.Create("schema", new SchemaProperties()) - .AddField(new NumberField(1, "field1", Partitioning.Language)) - .AddField(new NumberField(2, "field2", Partitioning.Invariant)) - .AddField(new NumberField(3, "field3", Partitioning.Invariant).Hide()) - .AddField(new AssetsField(5, "assets1", Partitioning.Invariant)) - .AddField(new AssetsField(6, "assets2", Partitioning.Invariant)) - .AddField(new JsonField(4, "json", Partitioning.Language)); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.EN, Language.DE); + private readonly Schema schema = new Schema("my-schema"); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + public ContentConversionTests() + { + schema.AddField(new NumberField(1, "field1", Partitioning.Language)); + schema.AddField(new NumberField(2, "field2", Partitioning.Invariant)); + schema.AddField(new NumberField(3, "field3", Partitioning.Invariant)); + + schema.AddField(new AssetsField(5, "assets1", Partitioning.Invariant)); + schema.AddField(new AssetsField(6, "assets2", Partitioning.Invariant)); + + schema.AddField(new JsonField(4, "json", Partitioning.Language)); + + schema.FieldsById[3].Hide(); + } [Fact] public void Should_convert_to_id_model() @@ -342,8 +351,9 @@ namespace Squidex.Domain.Apps.Core.Contents .AddValue("it", 7)); var fallbackConfig = - LanguagesConfig.Create(Language.DE).Add(Language.EN) - .Update(Language.DE, false, false, new[] { Language.EN }); + LanguagesConfig.Build( + new LanguageConfig(Language.EN), + new LanguageConfig(Language.DE, false, new[] { Language.EN })); var output = (Dictionary)data.ToLanguageModel(fallbackConfig, new List { Language.DE }); @@ -389,173 +399,6 @@ namespace Squidex.Domain.Apps.Core.Contents Assert.True(expected.EqualsDictionary(output)); } - [Fact] - public void Should_merge_two_name_models() - { - var lhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", 2)); - - var rhs = - new NamedContentData() - .AddField("field2", - new ContentFieldData() - .AddValue("en", 3)) - .AddField("field3", - new ContentFieldData() - .AddValue("iv", 4)); - - var expected = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", 2) - .AddValue("en", 3)) - .AddField("field3", - new ContentFieldData() - .AddValue("iv", 4)); - - var actual = lhs.MergeInto(rhs); - - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_merge_two_id_models() - { - var lhs = - new IdContentData() - .AddField(1, - new ContentFieldData() - .AddValue("iv", 1)) - .AddField(2, - new ContentFieldData() - .AddValue("de", 2)); - - var rhs = - new IdContentData() - .AddField(2, - new ContentFieldData() - .AddValue("en", 3)) - .AddField(3, - new ContentFieldData() - .AddValue("iv", 4)); - - var expected = - new IdContentData() - .AddField(1, - new ContentFieldData() - .AddValue("iv", 1)) - .AddField(2, - new ContentFieldData() - .AddValue("de", 2) - .AddValue("en", 3)) - .AddField(3, - new ContentFieldData() - .AddValue("iv", 4)); - - var actual = lhs.MergeInto(rhs); - - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_be_equal_when_data_have_same_structure() - { - var lhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - - var rhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - - Assert.True(lhs.Equals(rhs)); - Assert.True(lhs.Equals((object)rhs)); - Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); - } - - [Fact] - public void Should_not_be_equal_when_data_have_not_same_structure() - { - var lhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - - var rhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("en", 2)) - .AddField("field3", - new ContentFieldData() - .AddValue("iv", 2)); - - Assert.False(lhs.Equals(rhs)); - Assert.False(lhs.Equals((object)rhs)); - Assert.NotEqual(lhs.GetHashCode(), rhs.GetHashCode()); - } - - [Fact] - public void Should_remove_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new NamedContentData() - .AddField("assets1", - new ContentFieldData() - .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); - - var ids = input.GetReferencedIds(schema).ToArray(); - - Assert.Equal(new[] { id1, id2 }, ids); - } - - [Fact] - public void Should_cleanup_deleted_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new IdContentData() - .AddField(5, - new ContentFieldData() - .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); - - var actual = input.ToCleanedReferences(schema, new HashSet(new[] { id2 })); - - var cleanedValue = (JArray)actual[5]["iv"]; - - Assert.Equal(1, cleanedValue.Count); - Assert.Equal(id1.ToString(), cleanedValue[0]); - } - [Fact] public void Should_be_equal_fields_when_they_have_same_value() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs new file mode 100644 index 000000000..5f2b899d0 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs @@ -0,0 +1,202 @@ +// ========================================================================== +// ContentEnrichmentTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions + +namespace Squidex.Domain.Apps.Core.Operations.EnrichContent +{ + public class ContentEnrichmentTests + { + private static readonly Instant Now = SystemClock.Instance.GetCurrentInstant(); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + private readonly Schema schema; + + public ContentEnrichmentTests() + { + schema = new Schema("my-schema"); + + schema.AddField(new StringField(1, "my-string", Partitioning.Language, + new StringFieldProperties { DefaultValue = "en-string" })); + + schema.AddField(new NumberField(2, "my-number", Partitioning.Invariant, + new NumberFieldProperties())); + + schema.AddField(new DateTimeField(3, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = Now })); + + schema.AddField(new BooleanField(4, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true })); + } + + [Fact] + private void Should_enrich_with_default_values() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "de-string")) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal(456, (int)data["my-number"]["iv"]); + + Assert.Equal("de-string", (string)data["my-string"]["de"]); + Assert.Equal("en-string", (string)data["my-string"]["en"]); + + Assert.Equal(Now.ToString(), (string)data["my-datetime"]["iv"]); + + Assert.True((bool)data["my-boolean"]["iv"]); + } + + [Fact] + private void Should_also_enrich_with_default_values_when_string_is_empty() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", string.Empty)) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal("en-string", (string)data["my-string"]["de"]); + Assert.Equal("en-string", (string)data["my-string"]["en"]); + } + + [Fact] + public void Should_get_default_value_from_assets_field() + { + var field = + new AssetsField(1, "1", Partitioning.Invariant, + new AssetsFieldProperties()); + + Assert.Equal(new JArray(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_boolean_field() + { + var field = + new BooleanField(1, "1", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true }); + + Assert.Equal(true, DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field() + { + var field = + new DateTimeField(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); + + Assert.Equal(FutureDays(15).ToString(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_today() + { + var field = + new DateTimeField(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); + + Assert.Equal(Now.ToString().Substring(10), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_now() + { + var field = + new DateTimeField(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); + + Assert.Equal(Now.ToString(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_json_field() + { + var field = + new JsonField(1, "1", Partitioning.Invariant, + new JsonFieldProperties()); + + Assert.Equal(new JObject(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_geolocation_field() + { + var field = + new GeolocationField(1, "1", Partitioning.Invariant, + new GeolocationFieldProperties()); + + Assert.Equal(JValue.CreateNull(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_number_field() + { + var field = + new NumberField(1, "1", Partitioning.Invariant, + new NumberFieldProperties { DefaultValue = 12 }); + + Assert.Equal(12, DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_references_field() + { + var field = + new ReferencesField(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties()); + + Assert.Equal(new JArray(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_string_field() + { + var field = + new StringField(1, "1", Partitioning.Invariant, + new StringFieldProperties { DefaultValue = "default" }); + + Assert.Equal("default", DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_tags_field() + { + var field = + new TagsField(1, "1", Partitioning.Invariant, + new TagsFieldProperties()); + + Assert.Equal(new JArray(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + private static Instant FutureDays(int days) + { + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs new file mode 100644 index 000000000..7f4af1cd6 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -0,0 +1,243 @@ +// ========================================================================== +// ReferenceExtractionTests.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.Contents; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds +{ + public class ReferenceExtractionTests + { + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Schema schema = new Schema("my-schema"); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + public ReferenceExtractionTests() + { + schema.AddField(new NumberField(1, "field1", Partitioning.Language)); + schema.AddField(new NumberField(2, "field2", Partitioning.Invariant)); + schema.AddField(new NumberField(3, "field3", Partitioning.Invariant)); + + schema.AddField(new AssetsField(5, "assets1", Partitioning.Invariant)); + schema.AddField(new AssetsField(6, "assets2", Partitioning.Invariant)); + + schema.AddField(new JsonField(4, "json", Partitioning.Language)); + + schema.FieldsById[3].Hide(); + } + + [Fact] + public void Should_remove_ids() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); + + var ids = input.GetReferencedIds(schema).ToArray(); + + Assert.Equal(new[] { id1, id2 }, ids); + } + + [Fact] + public void Should_cleanup_deleted_ids() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); + + var actual = input.ToCleanedReferences(schema, new HashSet(new[] { id2 })); + + var cleanedValue = (JArray)actual[5]["iv"]; + + Assert.Equal(1, cleanedValue.Count); + Assert.Equal(id1.ToString(), cleanedValue[0]); + } + + [Fact] + public void Should_return_ids_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.ExtractReferences(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2 }, result); + } + + [Fact] + public void Should_empty_list_from_assets_field_for_referenced_ids_when_null() + { + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.ExtractReferences(null).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_empty_list_from_assets_field_for_referenced_ids_when_other_type() + { + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.ExtractReferences("invalid").ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_null_from_assets_field_when_removing_references_from_null_array() + { + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(null, null); + + Assert.Null(result); + } + + [Fact] + public void Should_remove_deleted_references_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); + + Assert.Equal(CreateValue(id1), result); + } + + [Fact] + public void Should_return_same_token_from_assets_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var token = CreateValue(id1, id2); + var result = sut.CleanReferences(token, new HashSet(new[] { Guid.NewGuid() })); + + Assert.Same(token, result); + } + + [Fact] + public void Should_return_ids_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.ExtractReferences(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2, schemaId }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null() + { + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.ExtractReferences(null).ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type() + { + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.ExtractReferences("invalid").ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_null_from_references_field_when_removing_references_from_null_array() + { + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); + + var result = sut.CleanReferences(null, null); + + Assert.Null(result); + } + + [Fact] + public void Should_remove_deleted_references_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); + + Assert.Equal(CreateValue(id1, schemaId), result); + } + + [Fact] + public void Should_remove_all_references_from_references_field_when_schema_is_removed() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), new HashSet(new[] { schemaId })); + + Assert.Equal(CreateValue(), result); + } + + [Fact] + public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); + + var token = CreateValue(id1, id2); + var result = sut.CleanReferences(token, new HashSet(new[] { Guid.NewGuid() })); + + Assert.Same(token, result); + } + + private static JToken CreateValue(params Guid[] ids) + { + return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs new file mode 100644 index 000000000..9bc7060ab --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// EdmTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.GenerateEdmSchema; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.GenerateEdmSchema +{ + public class EdmTests + { + [Fact] + public void Should_escape_field_name() + { + Assert.Equal("field_name", "field-name".EscapeEdmField()); + } + + [Fact] + public void Should_unescape_field_name() + { + Assert.Equal("field-name", "field_name".UnescapeEdmField()); + } + + [Fact] + public void Should_build_edm_model() + { + var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + + var edmModel = TestData.MixedSchema().BuildEdmType(languagesConfig.ToResolver(), x => x); + + Assert.NotNull(edmModel); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs new file mode 100644 index 000000000..508e88d38 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// JsonSchemaTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using NJsonSchema; +using Squidex.Domain.Apps.Core.GenerateJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema +{ + public class JsonSchemaTests + { + private readonly Schema schema = TestData.MixedSchema(); + + [Fact] + public void Should_build_json_schema() + { + var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + + var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema4 { Reference = s }); + + Assert.NotNull(jsonSchema); + } + + [Fact] + public void Should_build_data_schema() + { + var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + + var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema4 { Reference = s }); + + Assert.NotNull(new ContentSchemaBuilder().CreateContentSchema(schema, jsonSchema)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs similarity index 99% rename from tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs index 6b1f265e9..3325f39d9 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Xunit; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting { public sealed class ContentDataObjectTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index ec13177e4..7b957300c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -8,10 +8,11 @@ using System; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting { public class JintScriptEngineTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs similarity index 96% rename from tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs index af47df5f7..88d26d417 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs @@ -8,12 +8,13 @@ using System.Security.Claims; using Jint; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure.Security; using Xunit; #pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting { public class JintUserTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/AssetsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs similarity index 60% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/AssetsFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 3e293f939..6b1b43e6b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/AssetsFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -12,9 +12,10 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class AssetsFieldTests { @@ -28,14 +29,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-asset", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_assets_are_valid() { @@ -126,86 +119,6 @@ namespace Squidex.Domain.Apps.Core.Schemas new[] { $" contains invalid asset '{assetId}'." }); } - [Fact] - public void Should_return_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2 }, result); - } - - [Fact] - public void Should_empty_list_for_referenced_ids_when_null() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(null).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_empty_list_for_referenced_ids_when_other_type() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds("invalid").ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_array() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(null, null); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_json_array() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(JValue.CreateNull(), null); - - Assert.Null(result); - } - - [Fact] - public void Should_remove_deleted_references() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_return_same_token_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var token = CreateValue(id1, id2); - var result = sut.RemoveDeletedReferences(token, new HashSet(new[] { Guid.NewGuid() })); - - Assert.Same(token, result); - } - private static JToken CreateValue(params Guid[] ids) { return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/BooleanFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs similarity index 90% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/BooleanFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs index 8c03207be..aa96be3b7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/BooleanFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs @@ -10,9 +10,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class BooleanFieldTests { @@ -26,14 +27,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-bolean", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new BooleanField(1, "name", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_null_boolean_is_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs similarity index 83% rename from tests/Squidex.Domain.Apps.Core.Tests/ContentValidationTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 54f8f0d0f..7376cd51c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -11,17 +11,18 @@ using System.Threading.Tasks; using FluentAssertions; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class ContentValidationTests { - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); private readonly List errors = new List(); private readonly ValidationContext context = ValidationTestExtensions.ValidContext; - private Schema schema = Schema.Create("my-name", new SchemaProperties()); + private readonly Schema schema = new Schema("my-schema"); [Fact] public async Task Should_add_error_if_validating_data_with_unknown_field() @@ -43,13 +44,14 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_validating_data_with_invalid_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, + new NumberFieldProperties { MaxValue = 100 })); var data = new NamedContentData() .AddField("my-field", new ContentFieldData() - .SetValue(1000)); + .AddValue(1000)); await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); @@ -63,7 +65,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_non_localizable_data_field_contains_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); var data = new NamedContentData() @@ -85,7 +87,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_validating_data_with_invalid_localizable_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -103,7 +106,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_required_data_field_is_not_in_bag() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -120,7 +124,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_data_contains_invalid_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() @@ -142,9 +146,12 @@ namespace Squidex.Domain.Apps.Core public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language() { var optionalConfig = - LanguagesConfig.Create(Language.ES, Language.IT).Update(Language.IT, true, false, null); + LanguagesConfig.Build( + new LanguageConfig(Language.ES, false), + new LanguageConfig(Language.IT, true)); - schema = schema.AddField(new StringField(1, "my-field", Partitioning.Language, new StringFieldProperties { IsRequired = true })); + schema.AddField(new StringField(1, "my-field", Partitioning.Language, + new StringFieldProperties { IsRequired = true })); var data = new NamedContentData() @@ -160,7 +167,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_data_contains_unsupported_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() @@ -199,13 +206,13 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_validating_partial_data_with_invalid_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 })); var data = new NamedContentData() .AddField("my-field", new ContentFieldData() - .SetValue(1000)); + .AddValue(1000)); await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); @@ -219,7 +226,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); var data = new NamedContentData() @@ -241,7 +248,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -254,7 +262,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -267,7 +276,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_partial_data_contains_invalid_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() @@ -288,7 +297,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_partial_data_contains_unsupported_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimeFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimeFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs index 802cad054..5952cdfb1 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimeFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs @@ -12,9 +12,10 @@ using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class DateTimeFieldTests { @@ -28,14 +29,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-datetime", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new DateTimeField(1, "my-datetime", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_datetime_is_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs similarity index 85% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs index 99e305ee9..4421d93d7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs @@ -10,9 +10,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class GeolocationFieldTests { @@ -26,14 +27,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-geolocation", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_geolocation_is_valid_null() { @@ -59,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Schemas } [Fact] - public async Task Should_add_errors_if_geolocation_has_invalid_properties() + public async Task Should_add_errors_if_geolocation_has_invalid_latitude() { var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); @@ -73,6 +66,21 @@ namespace Squidex.Domain.Apps.Core.Schemas new[] { " is not a valid value." }); } + [Fact] + public async Task Should_add_errors_if_geolocation_has_invalid_longitude() + { + var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); + + var geolocation = new JObject( + new JProperty("latitude", 0), + new JProperty("longitude", 200)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is not a valid value." }); + } + [Fact] public async Task Should_add_errors_if_geolocation_has_too_many_properties() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/JsonFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs similarity index 86% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/JsonFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs index e4d649ee4..4e7b963f0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/JsonFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs @@ -10,9 +10,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class JsonFieldTests { @@ -26,14 +27,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-json", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new JsonField(1, "my-json", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_json_is_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/NumberFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs similarity index 90% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/NumberFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs index f631b0f64..33978a938 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/NumberFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs @@ -11,9 +11,10 @@ using System.Collections.Immutable; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class NumberFieldTests { @@ -27,14 +28,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-number", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new NumberField(1, "my-number", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_number_is_valid() { @@ -81,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Schemas [Fact] public async Task Should_add_errors_if_number_is_not_allowed() { - var sut = new NumberField(1, "my-number", Partitioning.Invariant, new NumberFieldProperties { AllowedValues = ImmutableList.Create(10d) }); + var sut = new NumberField(1, "my-number", Partitioning.Invariant, new NumberFieldProperties { AllowedValues = new[] { 10d } }); await sut.ValidateAsync(CreateValue(20), errors); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ReferencesFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs similarity index 56% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/ReferencesFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 2a0ef8df8..cbeb72435 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ReferencesFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -12,9 +12,10 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class ReferencesFieldTests { @@ -29,14 +30,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-refs", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_references_are_valid() { @@ -127,99 +120,6 @@ namespace Squidex.Domain.Apps.Core.Schemas new[] { $" contains invalid reference '{referenceId}'." }); } - [Fact] - public void Should_return_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2, schemaId }, result); - } - - [Fact] - public void Should_return_list_with_schema_idempty_list_for_referenced_ids_when_null() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(null).ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_list_with_schema_id_for_referenced_ids_when_other_type() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds("invalid").ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_array() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(null, null); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_json_array() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(JValue.CreateNull(), null); - - Assert.Null(result); - } - - [Fact] - public void Should_remove_deleted_references() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_remove_all_references_when_schema_is_removed() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet(new[] { schemaId })); - - Assert.Equal(CreateValue(), result); - } - - [Fact] - public void Should_return_same_token_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var token = CreateValue(id1, id2); - var result = sut.RemoveDeletedReferences(token, new HashSet(new[] { Guid.NewGuid() })); - - Assert.Same(token, result); - } - private static JToken CreateValue(params Guid[] ids) { return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/StringFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs similarity index 91% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/StringFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index 951ba27fe..eda35ca87 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/StringFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -11,9 +11,10 @@ using System.Collections.Immutable; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class StringFieldTests { @@ -27,14 +28,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-string", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new StringField(1, "my-string", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_string_is_valid() { @@ -81,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Schemas [Fact] public async Task Should_add_errors_if_string_not_allowed() { - var sut = new StringField(1, "my-string", Partitioning.Invariant, new StringFieldProperties { AllowedValues = ImmutableList.Create("Foo") }); + var sut = new StringField(1, "my-string", Partitioning.Invariant, new StringFieldProperties { AllowedValues = new[] { "Foo" } }); await sut.ValidateAsync(CreateValue("Bar"), errors); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/TagsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/TagsFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs index 299ab5b88..946ddc64b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/TagsFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -12,9 +12,10 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class TagsFieldTests { @@ -28,14 +29,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-tags", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new TagsField(1, "my-tags", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_tags_are_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs similarity index 90% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/ValidationTestExtensions.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index 92876ec30..054d1c6ec 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ValidationTestExtensions.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -10,9 +10,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.Schemas.Validators; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public static class ValidationTestExtensions { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs similarity index 91% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs index 7c015be0f..7031e8f72 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class AllowedValuesValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionItemValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs similarity index 92% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionItemValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs index f0e64e2f3..02ad49249 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionItemValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class CollectionItemValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs similarity index 94% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs index 07eac3007..f827d5be3 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class CollectionValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/PatternValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/PatternValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs index 5224cdd4e..8e905315f 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/PatternValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class PatternValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RangeValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs similarity index 94% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RangeValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs index fef036a4f..870f534fb 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RangeValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs @@ -10,9 +10,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class RangeValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs similarity index 94% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs index b5a5373cd..563988671 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public sealed class RequiredStringValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs similarity index 92% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs index 96a1a6b10..7333a0faf 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public sealed class RequiredValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs similarity index 95% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs index bed46e3be..1ce64bc0d 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs @@ -11,9 +11,10 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class StringLengthValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimePropertiesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimePropertiesTests.cs deleted file mode 100644 index c6e97c8f8..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimePropertiesTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// DateTimePropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class DateTimePropertiesTests - { - [Fact] - public void Should_provide_today_default_value() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }; - - Assert.Equal(DateTime.UtcNow.Date.ToString("o"), sut.GetDefaultValue().ToString()); - } - - [Fact] - public void Should_provide_now_default_value() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }; - - Assert.Equal(DateTime.UtcNow.ToString("o").Substring(0, 16), sut.GetDefaultValue().ToString().Substring(0, 16)); - } - - [Fact] - public void Should_provide_specific_default_value() - { - var sut = new DateTimeFieldProperties { DefaultValue = FutureDays(15) }; - - Assert.Equal(FutureDays(15).ToString(), sut.GetDefaultValue()); - } - - private static Instant FutureDays(int days) - { - return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldPropertiesTests.cs deleted file mode 100644 index 10b8fdd14..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldPropertiesTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ========================================================================== -// FieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class FieldPropertiesTests - { - private readonly List errors = new List(); - - public static IEnumerable Properties - { - get - { - yield return new AssetsFieldProperties(); - yield return new BooleanFieldProperties(); - yield return new DateTimeFieldProperties(); - yield return new GeolocationFieldProperties(); - yield return new JsonFieldProperties(); - yield return new NumberFieldProperties(); - yield return new ReferencesFieldProperties(); - yield return new StringFieldProperties(); - } - } - - public static IEnumerable PropertiesData - { - get { return Properties.Select(x => new object[] { x }); } - } - - [Theory] - [MemberData(nameof(PropertiesData))] - public void Should_set_or_freeze_sut(FieldProperties properties) - { - foreach (var property in properties.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) - { - var value = - property.PropertyType.GetTypeInfo().IsValueType ? - Activator.CreateInstance(property.PropertyType) : - null; - - property.SetValue(properties, value); - - var result = property.GetValue(properties); - - Assert.Equal(value, result); - } - - properties.Freeze(); - - foreach (var property in properties.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) - { - var value = - property.PropertyType.GetTypeInfo().IsValueType ? - Activator.CreateInstance(property.PropertyType) : - null; - - Assert.Throws(() => - { - try - { - property.SetValue(properties, value); - } - catch (Exception ex) - { - throw ex.InnerException; - } - }); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldRegistryTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldRegistryTests.cs deleted file mode 100644 index 07e486749..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldRegistryTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// FieldRegistryTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Newtonsoft.Json.Linq; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class FieldRegistryTests - { - private readonly FieldRegistry sut = new FieldRegistry(new TypeNameRegistry()); - - private sealed class InvalidProperties : FieldProperties - { - public override JToken GetDefaultValue() - { - return null; - } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return default(T); - } - } - - [Fact] - public void Should_throw_exception_if_creating_field_and_field_is_not_registered() - { - Assert.Throws(() => sut.CreateField(1, "name", Partitioning.Invariant, new InvalidProperties())); - } - - [Fact] - public void Should_create_field_by_properties() - { - var properties = new NumberFieldProperties(); - - var field = sut.CreateField(1, "name", Partitioning.Invariant, properties); - - Assert.Equal(properties, field.RawProperties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs deleted file mode 100644 index cfac9372d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// JsonSerializerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using FluentAssertions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas.Json -{ - public class JsonSerializerTests - { - private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); - private readonly JsonSerializer serializer; - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); - - public JsonSerializerTests() - { - serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - - serializerSettings.ContractResolver = new ConverterContractResolver( - new InstantConverter(), - new LanguageConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new RefTokenConverter(), - new SchemaConverter(new FieldRegistry(typeNameRegistry)), - new StringEnumConverter()); - - serializerSettings.TypeNameHandling = TypeNameHandling.Auto; - - serializer = JsonSerializer.Create(serializerSettings); - } - - [Fact] - public void Should_serialize_and_deserialize_schema() - { - var schema = - Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())).HideField(1) - .AddField(new AssetsField(2, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())).LockField(2) - .AddField(new StringField(3, "my-string1", Partitioning.Language, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ImmutableList.Create("a", "b") })) - .AddField(new StringField(4, "my-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" })) - .AddField(new NumberField(5, "my-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddField(new BooleanField(6, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties())).DisableField(3) - .AddField(new DateTimeField(7, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) - .AddField(new DateTimeField(8, "my-date", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })) - .AddField(new ReferencesField(9, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) - .AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())) - .AddField(new TagsField(11, "my-tags", Partitioning.Invariant, - new TagsFieldProperties())) - .Publish(); - - var deserialized = JToken.FromObject(schema, serializer).ToObject(serializer); - - deserialized.ShouldBeEquivalentTo(schema); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs deleted file mode 100644 index 6558cb88d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs +++ /dev/null @@ -1,281 +0,0 @@ -// ========================================================================== -// SchemaTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Newtonsoft.Json.Linq; -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas.Edm; -using Squidex.Domain.Apps.Core.Schemas.JsonSchema; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class SchemaTests - { - private Schema sut = Schema.Create("my-name", new SchemaProperties()); - - private sealed class InvalidProperties : FieldProperties - { - public override JToken GetDefaultValue() - { - return null; - } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return default(T); - } - } - - [Fact] - public void Should_instantiate_field() - { - var properties = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; - - var schema = Schema.Create("my-name", properties); - - Assert.Equal("my-name", schema.Name); - Assert.Equal(properties, schema.Properties); - } - - [Fact] - public void Should_throw_exception_if_creating_schema_with_invalid_name() - { - Assert.Throws(() => Schema.Create(string.Empty, new SchemaProperties())); - } - - [Fact] - public void Should_update_schema() - { - var properties = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; - - sut = sut.Update(properties); - - Assert.Equal(properties, sut.Properties); - } - - [Fact] - public void Should_add_field() - { - var field = Add(); - - Assert.Equal(field, sut.FieldsById[1]); - } - - [Fact] - public void Should_throw_exception_if_adding_field_with_name_that_already_exists() - { - Add(); - - Assert.Throws(() => sut.AddField(new NumberField(2, "my-field", Partitioning.Invariant))); - } - - [Fact] - public void Should_hide_field() - { - Add(); - - sut = sut.HideField(1); - sut = sut.HideField(1); - - Assert.True(sut.FieldsById[1].IsHidden); - } - - [Fact] - public void Should_show_field() - { - Add(); - - sut = sut.HideField(1); - sut = sut.ShowField(1); - sut = sut.ShowField(1); - - Assert.False(sut.FieldsById[1].IsHidden); - } - - [Fact] - public void Should_disable_field() - { - Add(); - - sut = sut.DisableField(1); - sut = sut.DisableField(1); - - Assert.True(sut.FieldsById[1].IsDisabled); - } - - [Fact] - public void Should_enable_field() - { - Add(); - - sut = sut.DisableField(1); - sut = sut.EnableField(1); - sut = sut.EnableField(1); - - Assert.False(sut.FieldsById[1].IsDisabled); - } - - [Fact] - public void Should_lock_field() - { - Add(); - - sut = sut.LockField(1); - - Assert.True(sut.FieldsById[1].IsLocked); - } - - [Fact] - public void Should_delete_field() - { - Add(); - - sut = sut.DeleteField(1); - - Assert.Empty(sut.FieldsById); - } - - [Fact] - public void Should_update_field() - { - Add(); - - sut = sut.UpdateField(1, new NumberFieldProperties { Hints = "my-hints" }); - - Assert.Equal("my-hints", sut.FieldsById[1].RawProperties.Hints); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() - { - Add(); - - Assert.Throws(() => sut.UpdateField(1, new StringFieldProperties())); - } - - [Fact] - public void Should_publish_schema() - { - sut = sut.Publish(); - - Assert.True(sut.IsPublished); - } - - [Fact] - public void Should_unpublish_schema() - { - sut = sut.Publish(); - sut = sut.Unpublish(); - - Assert.False(sut.IsPublished); - } - - [Fact] - public void Should_reorder_fields() - { - var field1 = new StringField(1, "1", Partitioning.Invariant); - var field2 = new StringField(2, "2", Partitioning.Invariant); - var field3 = new StringField(3, "3", Partitioning.Invariant); - - sut = sut.AddField(field1); - sut = sut.AddField(field2); - sut = sut.AddField(field3); - sut = sut.ReorderFields(new List { 3, 2, 1 }); - - Assert.Equal(new List { field3, field2, field1 }, sut.Fields.ToList()); - } - - [Fact] - public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() - { - var field1 = new StringField(1, "1", Partitioning.Invariant); - var field2 = new StringField(2, "2", Partitioning.Invariant); - - sut = sut.AddField(field1); - sut = sut.AddField(field2); - - Assert.Throws(() => sut.ReorderFields(new List { 1 })); - } - - [Fact] - public void Should_throw_exception_if_field_to_reorder_does_not_exist() - { - var field1 = new StringField(1, "1", Partitioning.Invariant); - var field2 = new StringField(2, "2", Partitioning.Invariant); - - sut = sut.AddField(field1); - sut = sut.AddField(field2); - - Assert.Throws(() => sut.ReorderFields(new List { 1, 4 })); - } - - [Fact] - public void Should_build_schema() - { - var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - - var jsonSchema = BuildMixedSchema().BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema4 { Reference = s }); - - Assert.NotNull(jsonSchema); - } - - [Fact] - public void Should_build_edm_model() - { - var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - - var edmModel = BuildMixedSchema().BuildEdmType(languagesConfig.ToResolver(), x => x); - - Assert.NotNull(edmModel); - } - - private static Schema BuildMixedSchema() - { - var schema = - Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())) - .AddField(new AssetsField(2, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new StringField(3, "my-string1", Partitioning.Language, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ImmutableList.Create("a", "b") })) - .AddField(new StringField(4, "my-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" })) - .AddField(new NumberField(5, "my-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddField(new BooleanField(6, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties())) - .AddField(new DateTimeField(7, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) - .AddField(new DateTimeField(8, "my-date", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })) - .AddField(new GeolocationField(9, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())) - .AddField(new ReferencesField(10, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties())) - .AddField(new TagsField(11, "my-tags", Partitioning.Invariant, - new TagsFieldProperties())); - - return schema; - } - - private NumberField Add() - { - var field = new NumberField(1, "my-field", Partitioning.Invariant); - - sut = sut.AddField(field); - - return field; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index e2b84e201..6314f5295 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -5,7 +5,8 @@ Squidex.Domain.Apps.Core - + + diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs new file mode 100644 index 000000000..0930c69da --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// TestData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core +{ + public static class TestData + { + public static Schema MixedSchema() + { + var inv = Partitioning.Invariant; + + var schema = new Schema("user"); + + schema.Publish(); + schema.Update(new SchemaProperties { Hints = "The User" }); + + schema.AddField(new JsonField(1, "my-json", inv, + new JsonFieldProperties())); + + schema.AddField(new AssetsField(2, "my-assets", inv, + new AssetsFieldProperties())); + + schema.AddField(new StringField(3, "my-string1", inv, + new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = new[] { "a", "b" } })); + + schema.AddField(new StringField(4, "my-string2", inv, + new StringFieldProperties { Hints = "My String1" })); + + schema.AddField(new NumberField(5, "my-number", inv, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 })); + + schema.AddField(new BooleanField(6, "my-boolean", inv, + new BooleanFieldProperties())); + + schema.AddField(new DateTimeField(7, "my-datetime", inv, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })); + + schema.AddField(new DateTimeField(8, "my-date", inv, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })); + + schema.AddField(new GeolocationField(9, "my-geolocation", inv, + new GeolocationFieldProperties())); + + schema.AddField(new ReferencesField(10, "my-references", inv, + new ReferencesFieldProperties())); + + schema.AddField(new TagsField(11, "my-tags", Partitioning.Language, + new TagsFieldProperties())); + + schema.FieldsById[7].Hide(); + schema.FieldsById[8].Disable(); + schema.FieldsById[9].Lock(); + + return schema; + } + } +}