mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
300 changed files with 11033 additions and 4384 deletions
@ -0,0 +1,59 @@ |
|||
// ==========================================================================
|
|||
// AppClient.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps |
|||
{ |
|||
public sealed class AppClient |
|||
{ |
|||
private readonly string secret; |
|||
private string name; |
|||
private AppClientPermission permission; |
|||
|
|||
public string Name |
|||
{ |
|||
get { return name; } |
|||
} |
|||
|
|||
public string Secret |
|||
{ |
|||
get { return secret; } |
|||
} |
|||
|
|||
public AppClientPermission Permission |
|||
{ |
|||
get { return permission; } |
|||
} |
|||
|
|||
public AppClient(string name, string secret, AppClientPermission permission) |
|||
{ |
|||
Guard.NotNullOrEmpty(name, nameof(name)); |
|||
Guard.NotNullOrEmpty(secret, nameof(secret)); |
|||
Guard.Enum(permission, nameof(permission)); |
|||
|
|||
this.name = name; |
|||
this.secret = secret; |
|||
this.permission = permission; |
|||
} |
|||
|
|||
public void Update(AppClientPermission newPermission) |
|||
{ |
|||
Guard.Enum(newPermission, nameof(newPermission)); |
|||
|
|||
permission = newPermission; |
|||
} |
|||
|
|||
public void Rename(string newName) |
|||
{ |
|||
Guard.NotNullOrEmpty(newName, nameof(newName)); |
|||
|
|||
name = newName; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// ==========================================================================
|
|||
// AppClients.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps |
|||
{ |
|||
public sealed class AppClients : DictionaryBase<string, AppClient> |
|||
{ |
|||
public void Add(string id, AppClient client) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
Guard.NotNull(client, nameof(client)); |
|||
|
|||
Inner.Add(id, client); |
|||
} |
|||
|
|||
public void Add(string id, string secret) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
Inner.Add(id, new AppClient(id, secret, AppClientPermission.Editor)); |
|||
} |
|||
|
|||
public void Revoke(string id) |
|||
{ |
|||
Guard.NotNullOrEmpty(id, nameof(id)); |
|||
|
|||
Inner.Remove(id); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// ==========================================================================
|
|||
// AppContributors.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps |
|||
{ |
|||
public sealed class AppContributors : DictionaryBase<string, AppContributorPermission> |
|||
{ |
|||
public void Assign(string contributorId, AppContributorPermission permission) |
|||
{ |
|||
Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); |
|||
Guard.Enum(permission, nameof(permission)); |
|||
|
|||
Inner[contributorId] = permission; |
|||
} |
|||
|
|||
public void Remove(string contributorId) |
|||
{ |
|||
Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); |
|||
|
|||
Inner.Remove(contributorId); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
// ==========================================================================
|
|||
// AppPlan.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps |
|||
{ |
|||
public sealed class AppPlan |
|||
{ |
|||
public RefToken Owner { get; } |
|||
|
|||
public string PlanId { get; } |
|||
|
|||
public AppPlan(RefToken owner, string planId) |
|||
{ |
|||
Guard.NotNull(owner, nameof(owner)); |
|||
Guard.NotNullOrEmpty(planId, nameof(planId)); |
|||
|
|||
Owner = owner; |
|||
|
|||
PlanId = planId; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
// ==========================================================================
|
|||
// AppClientsConverter.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Newtonsoft.Json; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps.Json |
|||
{ |
|||
public sealed class AppClientsConverter : JsonClassConverter<AppClients> |
|||
{ |
|||
protected override void WriteValue(JsonWriter writer, AppClients value, JsonSerializer serializer) |
|||
{ |
|||
var json = new Dictionary<string, JsonAppClient>(value.Count); |
|||
|
|||
foreach (var client in value) |
|||
{ |
|||
json.Add(client.Key, new JsonAppClient(client.Value)); |
|||
} |
|||
|
|||
serializer.Serialize(writer, json); |
|||
} |
|||
|
|||
protected override AppClients ReadValue(JsonReader reader, JsonSerializer serializer) |
|||
{ |
|||
var json = serializer.Deserialize<Dictionary<string, JsonAppClient>>(reader); |
|||
|
|||
var clients = new AppClients(); |
|||
|
|||
foreach (var client in json) |
|||
{ |
|||
clients.Add(client.Key, client.Value.ToClient()); |
|||
} |
|||
|
|||
return clients; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
// ==========================================================================
|
|||
// AppContributorsConverter.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Newtonsoft.Json; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps.Json |
|||
{ |
|||
public sealed class AppContributorsConverter : JsonClassConverter<AppContributors> |
|||
{ |
|||
protected override void WriteValue(JsonWriter writer, AppContributors value, JsonSerializer serializer) |
|||
{ |
|||
var json = new Dictionary<string, AppContributorPermission>(value.Count); |
|||
|
|||
foreach (var contributor in value) |
|||
{ |
|||
json.Add(contributor.Key, contributor.Value); |
|||
} |
|||
|
|||
serializer.Serialize(writer, json); |
|||
} |
|||
|
|||
protected override AppContributors ReadValue(JsonReader reader, JsonSerializer serializer) |
|||
{ |
|||
var json = serializer.Deserialize<Dictionary<string, AppContributorPermission>>(reader); |
|||
|
|||
var contributors = new AppContributors(); |
|||
|
|||
foreach (var contributor in json) |
|||
{ |
|||
contributors.Assign(contributor.Key, contributor.Value); |
|||
} |
|||
|
|||
return contributors; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
// ==========================================================================
|
|||
// JsonAppClient.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Newtonsoft.Json; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps.Json |
|||
{ |
|||
public class JsonAppClient |
|||
{ |
|||
[JsonProperty] |
|||
public string Name { get; set; } |
|||
|
|||
[JsonProperty] |
|||
public string Secret { get; set; } |
|||
|
|||
[JsonProperty] |
|||
public AppClientPermission Permission { get; set; } |
|||
|
|||
public JsonAppClient() |
|||
{ |
|||
} |
|||
|
|||
public JsonAppClient(AppClient client) |
|||
{ |
|||
SimpleMapper.Map(client, this); |
|||
} |
|||
|
|||
public AppClient ToClient() |
|||
{ |
|||
return new AppClient(Name, Secret, Permission); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// ==========================================================================
|
|||
// JsonLanguageConfig.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Linq; |
|||
using Newtonsoft.Json; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps.Json |
|||
{ |
|||
public class JsonLanguageConfig |
|||
{ |
|||
[JsonProperty] |
|||
public Language[] Fallback { get; set; } |
|||
|
|||
[JsonProperty] |
|||
public bool IsOptional { get; set; } |
|||
|
|||
public JsonLanguageConfig() |
|||
{ |
|||
} |
|||
|
|||
public JsonLanguageConfig(LanguageConfig config) |
|||
{ |
|||
SimpleMapper.Map(config, this); |
|||
|
|||
Fallback = config.LanguageFallbacks.ToArray(); |
|||
} |
|||
|
|||
public LanguageConfig ToConfig(string language) |
|||
{ |
|||
return new LanguageConfig(language, IsOptional, Fallback); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
// ==========================================================================
|
|||
// AppClientsConverter.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Newtonsoft.Json; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps.Json |
|||
{ |
|||
public sealed class LanguagesConfigConverter : JsonClassConverter<LanguagesConfig> |
|||
{ |
|||
protected override void WriteValue(JsonWriter writer, LanguagesConfig value, JsonSerializer serializer) |
|||
{ |
|||
var json = new Dictionary<string, JsonLanguageConfig>(value.Count); |
|||
|
|||
foreach (LanguageConfig config in value) |
|||
{ |
|||
json.Add(config.Language, new JsonLanguageConfig(config)); |
|||
} |
|||
|
|||
serializer.Serialize(writer, json); |
|||
} |
|||
|
|||
protected override LanguagesConfig ReadValue(JsonReader reader, JsonSerializer serializer) |
|||
{ |
|||
var json = serializer.Deserialize<Dictionary<string, JsonLanguageConfig>>(reader); |
|||
|
|||
var languagesConfig = new LanguageConfig[json.Count]; |
|||
|
|||
var i = 0; |
|||
|
|||
foreach (var config in json) |
|||
{ |
|||
languagesConfig[i++] = config.Value.ToConfig(config.Key); |
|||
} |
|||
|
|||
return LanguagesConfig.Build(languagesConfig); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
// ==========================================================================
|
|||
// LanguageConfig.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps |
|||
{ |
|||
public sealed class LanguageConfig : IFieldPartitionItem |
|||
{ |
|||
private readonly Language language; |
|||
private readonly Language[] languageFallbacks; |
|||
private readonly bool isOptional; |
|||
|
|||
public bool IsOptional |
|||
{ |
|||
get { return isOptional; } |
|||
} |
|||
|
|||
public Language Language |
|||
{ |
|||
get { return language; } |
|||
} |
|||
|
|||
public IEnumerable<Language> LanguageFallbacks |
|||
{ |
|||
get { return languageFallbacks; } |
|||
} |
|||
|
|||
string IFieldPartitionItem.Key |
|||
{ |
|||
get { return language.Iso2Code; } |
|||
} |
|||
|
|||
string IFieldPartitionItem.Name |
|||
{ |
|||
get { return language.EnglishName; } |
|||
} |
|||
|
|||
IEnumerable<string> IFieldPartitionItem.Fallback |
|||
{ |
|||
get { return LanguageFallbacks.Select(x => x.Iso2Code); } |
|||
} |
|||
|
|||
public LanguageConfig(Language language, bool isOptional = false, IEnumerable<Language> fallback = null) |
|||
: this(language, isOptional, fallback?.ToArray()) |
|||
{ |
|||
} |
|||
|
|||
public LanguageConfig(Language language, bool isOptional = false, params Language[] fallback) |
|||
{ |
|||
Guard.NotNull(language, nameof(language)); |
|||
|
|||
this.isOptional = isOptional; |
|||
|
|||
this.language = language; |
|||
this.languageFallbacks = fallback ?? new Language[0]; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
// ==========================================================================
|
|||
// LanguagesConfig.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable IDE0016 // Use 'throw' expression
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Apps |
|||
{ |
|||
public sealed class LanguagesConfig : IFieldPartitioning |
|||
{ |
|||
private State state; |
|||
|
|||
public LanguageConfig Master |
|||
{ |
|||
get { return state.Master; } |
|||
} |
|||
|
|||
IFieldPartitionItem IFieldPartitioning.Master |
|||
{ |
|||
get { return state.Master; } |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return state.Languages.Values.GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator() |
|||
{ |
|||
return state.Languages.Values.GetEnumerator(); |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get { return state.Languages.Count; } |
|||
} |
|||
|
|||
private LanguagesConfig(ICollection<LanguageConfig> configs) |
|||
{ |
|||
Guard.NotNull(configs, nameof(configs)); |
|||
|
|||
state = new State(configs.ToImmutableDictionary(x => x.Language), configs.FirstOrDefault()); |
|||
} |
|||
|
|||
public static LanguagesConfig Build(params LanguageConfig[] configs) |
|||
{ |
|||
Guard.NotNull(configs, nameof(configs)); |
|||
|
|||
return new LanguagesConfig(configs); |
|||
} |
|||
|
|||
public static LanguagesConfig Build(params Language[] languages) |
|||
{ |
|||
Guard.NotNull(languages, nameof(languages)); |
|||
|
|||
return new LanguagesConfig(languages.Select(x => new LanguageConfig(x, false)).ToList()); |
|||
} |
|||
|
|||
public void MakeMaster(Language language) |
|||
{ |
|||
Guard.NotNull(language, nameof(language)); |
|||
|
|||
state = new State(state.Languages, state.Languages[language]); |
|||
} |
|||
|
|||
public void Set(LanguageConfig config) |
|||
{ |
|||
Guard.NotNull(config, nameof(config)); |
|||
|
|||
state = new State(state.Languages.SetItem(config.Language, config), state.Master?.Language == config.Language ? config : state.Master); |
|||
} |
|||
|
|||
public void Remove(Language language) |
|||
{ |
|||
Guard.NotNull(language, nameof(language)); |
|||
|
|||
var newLanguages = |
|||
state.Languages.Values.Where(x => x.Language != language) |
|||
.Select(config => |
|||
{ |
|||
return new LanguageConfig( |
|||
config.Language, |
|||
config.IsOptional, |
|||
config.LanguageFallbacks.Except(new[] { language })); |
|||
}) |
|||
.ToImmutableDictionary(x => x.Language); |
|||
|
|||
var newMaster = |
|||
state.Master.Language != language ? |
|||
state.Master : |
|||
state.Languages.Values.FirstOrDefault(); |
|||
|
|||
state = new State(newLanguages, newMaster); |
|||
} |
|||
|
|||
public bool Contains(Language language) |
|||
{ |
|||
return language != null && state.Languages.ContainsKey(language); |
|||
} |
|||
|
|||
public bool TryGetConfig(Language language, out LanguageConfig config) |
|||
{ |
|||
return state.Languages.TryGetValue(language, out config); |
|||
} |
|||
|
|||
public bool TryGetItem(string key, out IFieldPartitionItem item) |
|||
{ |
|||
if (Language.IsValidLanguage(key) && state.Languages.TryGetValue(key, out var value)) |
|||
{ |
|||
item = value; |
|||
|
|||
return true; |
|||
} |
|||
else |
|||
{ |
|||
item = null; |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
|
|||
private sealed class State |
|||
{ |
|||
public ImmutableDictionary<Language, LanguageConfig> Languages { get; } |
|||
|
|||
public LanguageConfig Master { get; } |
|||
|
|||
public State(ImmutableDictionary<Language, LanguageConfig> languages, LanguageConfig master) |
|||
{ |
|||
foreach (var languageConfig in languages.Values) |
|||
{ |
|||
foreach (var fallback in languageConfig.LanguageFallbacks) |
|||
{ |
|||
if (!languages.ContainsKey(fallback)) |
|||
{ |
|||
var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; |
|||
|
|||
throw new InvalidOperationException(message); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Languages = languages; |
|||
|
|||
if (master == null) |
|||
{ |
|||
throw new InvalidOperationException("Config has no master language."); |
|||
} |
|||
|
|||
this.Master = master; |
|||
} |
|||
} |
|||
|
|||
public PartitionResolver ToResolver() |
|||
{ |
|||
return partitioning => |
|||
{ |
|||
if (partitioning.Equals(Partitioning.Invariant)) |
|||
{ |
|||
return InvariantPartitioning.Instance; |
|||
} |
|||
|
|||
return this; |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
// ==========================================================================
|
|||
// ContentData.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public abstract class ContentData<T> : Dictionary<T, ContentFieldData>, IEquatable<ContentData<T>> |
|||
{ |
|||
public IEnumerable<KeyValuePair<T, ContentFieldData>> ValidValues |
|||
{ |
|||
get { return this.Where(x => x.Value != null); } |
|||
} |
|||
|
|||
protected ContentData(IEqualityComparer<T> comparer) |
|||
: base(comparer) |
|||
{ |
|||
} |
|||
|
|||
protected ContentData(IDictionary<T, ContentFieldData> copy, IEqualityComparer<T> comparer) |
|||
: base(copy, comparer) |
|||
{ |
|||
} |
|||
|
|||
protected static TResult Merge<TResult>(TResult source, TResult target) where TResult : ContentData<T> |
|||
{ |
|||
if (ReferenceEquals(target, source)) |
|||
{ |
|||
return source; |
|||
} |
|||
|
|||
foreach (var otherValue in source) |
|||
{ |
|||
var fieldValue = target.GetOrAdd(otherValue.Key, x => new ContentFieldData()); |
|||
|
|||
foreach (var value in otherValue.Value) |
|||
{ |
|||
fieldValue[value.Key] = value.Value; |
|||
} |
|||
} |
|||
|
|||
return target; |
|||
} |
|||
|
|||
protected static TResult Clean<TResult>(TResult source, TResult target) where TResult : ContentData<T> |
|||
{ |
|||
foreach (var fieldValue in source.ValidValues) |
|||
{ |
|||
var resultValue = new ContentFieldData(); |
|||
|
|||
foreach (var partitionValue in fieldValue.Value.Where(x => !x.Value.IsNull())) |
|||
{ |
|||
resultValue[partitionValue.Key] = partitionValue.Value; |
|||
} |
|||
|
|||
if (resultValue.Count > 0) |
|||
{ |
|||
target[fieldValue.Key] = resultValue; |
|||
} |
|||
} |
|||
|
|||
return target; |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return Equals(obj as ContentData<T>); |
|||
} |
|||
|
|||
public bool Equals(ContentData<T> other) |
|||
{ |
|||
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return this.DictionaryHashCode(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// ==========================================================================
|
|||
// ContentFieldData.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public sealed class ContentFieldData : Dictionary<string, JToken>, IEquatable<ContentFieldData> |
|||
{ |
|||
private static readonly JTokenEqualityComparer JTokenEqualityComparer = new JTokenEqualityComparer(); |
|||
|
|||
public ContentFieldData() |
|||
: base(StringComparer.OrdinalIgnoreCase) |
|||
{ |
|||
} |
|||
|
|||
public ContentFieldData AddValue(string key, JToken value) |
|||
{ |
|||
Guard.NotNullOrEmpty(key, nameof(key)); |
|||
|
|||
this[key] = value; |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public ContentFieldData AddValue(JToken value) |
|||
{ |
|||
return AddValue(InvariantPartitioning.Instance.Master.Key, value); |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return Equals(obj as ContentFieldData); |
|||
} |
|||
|
|||
public bool Equals(ContentFieldData other) |
|||
{ |
|||
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other, EqualityComparer<string>.Default, JTokenEqualityComparer)); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return this.DictionaryHashCode(EqualityComparer<string>.Default, JTokenEqualityComparer); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
// ==========================================================================
|
|||
// IdContentData.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public sealed class IdContentData : ContentData<long>, IEquatable<IdContentData> |
|||
{ |
|||
public IdContentData() |
|||
: base(EqualityComparer<long>.Default) |
|||
{ |
|||
} |
|||
|
|||
public IdContentData(IdContentData copy) |
|||
: base(copy, EqualityComparer<long>.Default) |
|||
{ |
|||
} |
|||
|
|||
public IdContentData MergeInto(IdContentData target) |
|||
{ |
|||
return Merge(this, target); |
|||
} |
|||
|
|||
public IdContentData ToCleaned() |
|||
{ |
|||
return Clean(this, new IdContentData()); |
|||
} |
|||
|
|||
public IdContentData AddField(long id, ContentFieldData data) |
|||
{ |
|||
Guard.GreaterThan(id, 0, nameof(id)); |
|||
|
|||
this[id] = data; |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public bool Equals(IdContentData other) |
|||
{ |
|||
return base.Equals(other); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
// ==========================================================================
|
|||
// NamedContentData.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public sealed class NamedContentData : ContentData<string>, IEquatable<NamedContentData> |
|||
{ |
|||
public NamedContentData() |
|||
: base(StringComparer.OrdinalIgnoreCase) |
|||
{ |
|||
} |
|||
|
|||
public NamedContentData MergeInto(NamedContentData target) |
|||
{ |
|||
return Merge(this, target); |
|||
} |
|||
|
|||
public NamedContentData ToCleaned() |
|||
{ |
|||
return Clean(this, new NamedContentData()); |
|||
} |
|||
|
|||
public NamedContentData AddField(string name, ContentFieldData data) |
|||
{ |
|||
Guard.NotNullOrEmpty(name, nameof(name)); |
|||
|
|||
this[name] = data; |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public bool Equals(NamedContentData other) |
|||
{ |
|||
return base.Equals(other); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
// ==========================================================================
|
|||
// StatusFlow.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public static class StatusFlow |
|||
{ |
|||
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]> |
|||
{ |
|||
[Status.Draft] = new[] { Status.Published, Status.Archived }, |
|||
[Status.Archived] = new[] { Status.Draft }, |
|||
[Status.Published] = new[] { Status.Draft, Status.Archived } |
|||
}; |
|||
|
|||
public static bool Exists(Status status) |
|||
{ |
|||
return Flow.ContainsKey(status); |
|||
} |
|||
|
|||
public static bool CanChange(Status status, Status toStatus) |
|||
{ |
|||
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
// ==========================================================================
|
|||
// DictionaryBase.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Core |
|||
{ |
|||
public abstract class DictionaryBase<TKey, TValue> : IReadOnlyDictionary<TKey, TValue> |
|||
{ |
|||
private readonly Dictionary<TKey, TValue> inner = new Dictionary<TKey, TValue>(); |
|||
|
|||
public TValue this[TKey key] |
|||
{ |
|||
get { return inner[key]; } |
|||
} |
|||
|
|||
public IEnumerable<TKey> Keys |
|||
{ |
|||
get { return inner.Keys; } |
|||
} |
|||
|
|||
public IEnumerable<TValue> Values |
|||
{ |
|||
get { return inner.Values; } |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get { return inner.Count; } |
|||
} |
|||
|
|||
protected Dictionary<TKey, TValue> Inner |
|||
{ |
|||
get { return inner; } |
|||
} |
|||
|
|||
public bool ContainsKey(TKey key) |
|||
{ |
|||
return inner.ContainsKey(key); |
|||
} |
|||
|
|||
public bool TryGetValue(TKey key, out TValue value) |
|||
{ |
|||
return inner.TryGetValue(key, out value); |
|||
} |
|||
|
|||
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() |
|||
{ |
|||
return inner.GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return inner.GetEnumerator(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +1,23 @@ |
|||
// ==========================================================================
|
|||
// IAppClientEntity.cs
|
|||
// IFieldPartitionItem.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Read.Apps |
|||
namespace Squidex.Domain.Apps.Core |
|||
{ |
|||
public interface IAppClientEntity |
|||
public interface IFieldPartitionItem |
|||
{ |
|||
string Key { get; } |
|||
|
|||
string Name { get; } |
|||
|
|||
string Secret { get; } |
|||
bool IsOptional { get; } |
|||
|
|||
AppClientPermission Permission { get; } |
|||
IEnumerable<string> Fallback { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// IFieldPartitioning.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Core |
|||
{ |
|||
public interface IFieldPartitioning : IReadOnlyCollection<IFieldPartitionItem> |
|||
{ |
|||
IFieldPartitionItem Master { get; } |
|||
|
|||
bool TryGetItem(string key, out IFieldPartitionItem item); |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
// ==========================================================================
|
|||
// InvariantPartitioning.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Squidex.Domain.Apps.Core |
|||
{ |
|||
public sealed class InvariantPartitioning : IFieldPartitioning, IFieldPartitionItem |
|||
{ |
|||
public static readonly InvariantPartitioning Instance = new InvariantPartitioning(); |
|||
|
|||
public int Count |
|||
{ |
|||
get { return 1; } |
|||
} |
|||
|
|||
public IFieldPartitionItem Master |
|||
{ |
|||
get { return this; } |
|||
} |
|||
|
|||
string IFieldPartitionItem.Key |
|||
{ |
|||
get { return "iv"; } |
|||
} |
|||
|
|||
string IFieldPartitionItem.Name |
|||
{ |
|||
get { return "Invariant"; } |
|||
} |
|||
|
|||
bool IFieldPartitionItem.IsOptional |
|||
{ |
|||
get { return false; } |
|||
} |
|||
|
|||
IEnumerable<string> IFieldPartitionItem.Fallback |
|||
{ |
|||
get { return Enumerable.Empty<string>(); } |
|||
} |
|||
|
|||
private InvariantPartitioning() |
|||
{ |
|||
} |
|||
|
|||
public bool TryGetItem(string key, out IFieldPartitionItem item) |
|||
{ |
|||
var isFound = string.Equals(key, "iv", StringComparison.OrdinalIgnoreCase); |
|||
|
|||
item = isFound ? this : null; |
|||
|
|||
return isFound; |
|||
} |
|||
|
|||
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator() |
|||
{ |
|||
yield return this; |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
yield return this; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
// ==========================================================================
|
|||
// Partitioning.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core |
|||
{ |
|||
public delegate IFieldPartitioning PartitionResolver(Partitioning key); |
|||
|
|||
public sealed class Partitioning : IEquatable<Partitioning> |
|||
{ |
|||
public static readonly Partitioning Invariant = new Partitioning("invariant"); |
|||
public static readonly Partitioning Language = new Partitioning("language"); |
|||
|
|||
public string Key { get; } |
|||
|
|||
public Partitioning(string key) |
|||
{ |
|||
Guard.NotNullOrEmpty(key, nameof(key)); |
|||
|
|||
Key = key; |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
return Equals(obj as Partitioning); |
|||
} |
|||
|
|||
public bool Equals(Partitioning other) |
|||
{ |
|||
return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
return Key.GetHashCode(); |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return Key; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
// ==========================================================================
|
|||
// PartitioningExtensions.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Core |
|||
{ |
|||
public static class PartitioningExtensions |
|||
{ |
|||
private static readonly HashSet<string> AllowedPartitions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) |
|||
{ |
|||
Partitioning.Language.Key, |
|||
Partitioning.Invariant.Key |
|||
}; |
|||
|
|||
public static bool IsValidPartitioning(this string value) |
|||
{ |
|||
return value == null || AllowedPartitions.Contains(value); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// AssetsField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class AssetsField : Field<AssetsFieldProperties> |
|||
{ |
|||
public AssetsField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new AssetsFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public AssetsField(long id, string name, Partitioning partitioning, AssetsFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// AssetsFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(AssetsField))] |
|||
public sealed class AssetsFieldProperties : FieldProperties |
|||
{ |
|||
public int? MinItems { get; set; } |
|||
|
|||
public int? MaxItems { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// BooleanField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class BooleanField : Field<BooleanFieldProperties> |
|||
{ |
|||
public BooleanField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new BooleanFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public BooleanField(long id, string name, Partitioning partitioning, BooleanFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// BooleanFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(BooleanField))] |
|||
public sealed class BooleanFieldProperties : FieldProperties |
|||
{ |
|||
public bool? DefaultValue { get; set; } |
|||
|
|||
public BooleanFieldEditor Editor { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// DateTimeField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class DateTimeField : Field<DateTimeFieldProperties> |
|||
{ |
|||
public DateTimeField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new DateTimeFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public DateTimeField(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// ==========================================================================
|
|||
// DateTimeFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using NodaTime; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(DateTimeField))] |
|||
public sealed class DateTimeFieldProperties : FieldProperties |
|||
{ |
|||
public Instant? MaxValue { get; set; } |
|||
|
|||
public Instant? MinValue { get; set; } |
|||
|
|||
public Instant? DefaultValue { get; set; } |
|||
|
|||
public DateTimeFieldEditor Editor { get; set; } |
|||
|
|||
public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
// ==========================================================================
|
|||
// Field.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public abstract class Field |
|||
{ |
|||
private readonly long fieldId; |
|||
private readonly Partitioning partitioning; |
|||
private readonly string fieldName; |
|||
private bool isDisabled; |
|||
private bool isHidden; |
|||
private bool isLocked; |
|||
|
|||
public long Id |
|||
{ |
|||
get { return fieldId; } |
|||
} |
|||
|
|||
public string Name |
|||
{ |
|||
get { return fieldName; } |
|||
} |
|||
|
|||
public bool IsLocked |
|||
{ |
|||
get { return isLocked; } |
|||
} |
|||
|
|||
public bool IsHidden |
|||
{ |
|||
get { return isHidden; } |
|||
} |
|||
|
|||
public bool IsDisabled |
|||
{ |
|||
get { return isDisabled; } |
|||
} |
|||
|
|||
public Partitioning Partitioning |
|||
{ |
|||
get { return partitioning; } |
|||
} |
|||
|
|||
public abstract FieldProperties RawProperties { get; } |
|||
|
|||
protected Field(long id, string name, Partitioning partitioning) |
|||
{ |
|||
Guard.NotNullOrEmpty(name, nameof(name)); |
|||
Guard.NotNull(partitioning, nameof(partitioning)); |
|||
Guard.GreaterThan(id, 0, nameof(id)); |
|||
|
|||
fieldId = id; |
|||
fieldName = name; |
|||
|
|||
this.partitioning = partitioning; |
|||
} |
|||
|
|||
public void Lock() |
|||
{ |
|||
isLocked = true; |
|||
} |
|||
|
|||
public void Hide() |
|||
{ |
|||
isHidden = true; |
|||
} |
|||
|
|||
public void Show() |
|||
{ |
|||
isHidden = false; |
|||
} |
|||
|
|||
public void Disable() |
|||
{ |
|||
isDisabled = true; |
|||
} |
|||
|
|||
public void Enable() |
|||
{ |
|||
isDisabled = false; |
|||
} |
|||
|
|||
public abstract void Update(FieldProperties newProperties); |
|||
|
|||
public abstract T Accept<T>(IFieldVisitor<T> visitor); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// FieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public abstract class FieldProperties : NamedElementPropertiesBase |
|||
{ |
|||
public bool IsRequired { get; set; } |
|||
|
|||
public bool IsListField { get; set; } |
|||
|
|||
public string Placeholder { get; set; } |
|||
|
|||
public abstract T Accept<T>(IFieldPropertiesVisitor<T> visitor); |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
// ==========================================================================
|
|||
// FieldRegistry.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class FieldRegistry |
|||
{ |
|||
private delegate Field FactoryFunction(long id, string name, Partitioning partitioning, FieldProperties properties); |
|||
|
|||
private readonly TypeNameRegistry typeNameRegistry; |
|||
private readonly Dictionary<Type, Registered> fieldsByPropertyType = new Dictionary<Type, Registered>(); |
|||
|
|||
private sealed class Registered |
|||
{ |
|||
private readonly FactoryFunction fieldFactory; |
|||
private readonly Type propertiesType; |
|||
|
|||
public Type PropertiesType |
|||
{ |
|||
get { return propertiesType; } |
|||
} |
|||
|
|||
public Registered(FactoryFunction fieldFactory, Type propertiesType) |
|||
{ |
|||
this.fieldFactory = fieldFactory; |
|||
this.propertiesType = propertiesType; |
|||
} |
|||
|
|||
public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties) |
|||
{ |
|||
return fieldFactory(id, name, partitioning, properties); |
|||
} |
|||
} |
|||
|
|||
public FieldRegistry(TypeNameRegistry typeNameRegistry) |
|||
{ |
|||
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); |
|||
|
|||
this.typeNameRegistry = typeNameRegistry; |
|||
|
|||
Add<BooleanFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new BooleanField(id, name, partitioning, (BooleanFieldProperties)properties)); |
|||
|
|||
Add<NumberFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new NumberField(id, name, partitioning, (NumberFieldProperties)properties)); |
|||
|
|||
Add<StringFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new StringField(id, name, partitioning, (StringFieldProperties)properties)); |
|||
|
|||
Add<JsonFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new JsonField(id, name, partitioning, (JsonFieldProperties)properties)); |
|||
|
|||
Add<AssetsFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new AssetsField(id, name, partitioning, (AssetsFieldProperties)properties)); |
|||
|
|||
Add<GeolocationFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new GeolocationField(id, name, partitioning, (GeolocationFieldProperties)properties)); |
|||
|
|||
Add<ReferencesFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new ReferencesField(id, name, partitioning, (ReferencesFieldProperties)properties)); |
|||
|
|||
Add<DateTimeFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new DateTimeField(id, name, partitioning, (DateTimeFieldProperties)properties)); |
|||
|
|||
Add<TagsFieldProperties>( |
|||
(id, name, partitioning, properties) => |
|||
new TagsField(id, name, partitioning, (TagsFieldProperties)properties)); |
|||
|
|||
typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime"); |
|||
|
|||
typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References"); |
|||
} |
|||
|
|||
private void Add<TFieldProperties>(FactoryFunction fieldFactory) |
|||
{ |
|||
Guard.NotNull(fieldFactory, nameof(fieldFactory)); |
|||
|
|||
typeNameRegistry.Map(typeof(TFieldProperties)); |
|||
|
|||
var registered = new Registered(fieldFactory, typeof(TFieldProperties)); |
|||
|
|||
fieldsByPropertyType[registered.PropertiesType] = registered; |
|||
} |
|||
|
|||
public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties) |
|||
{ |
|||
Guard.NotNull(properties, nameof(properties)); |
|||
|
|||
var registered = fieldsByPropertyType.GetOrDefault(properties.GetType()); |
|||
|
|||
if (registered == null) |
|||
{ |
|||
throw new InvalidOperationException($"The field property '{properties.GetType()}' is not supported."); |
|||
} |
|||
|
|||
return registered.CreateField(id, name, partitioning, properties); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// ==========================================================================
|
|||
// Field_Generic.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public abstract class Field<T> : Field where T : FieldProperties, new() |
|||
{ |
|||
private T properties; |
|||
|
|||
public T Properties |
|||
{ |
|||
get { return properties; } |
|||
} |
|||
|
|||
public override FieldProperties RawProperties |
|||
{ |
|||
get { return properties; } |
|||
} |
|||
|
|||
protected Field(long id, string name, Partitioning partitioning, T properties) |
|||
: base(id, name, partitioning) |
|||
{ |
|||
Guard.NotNull(properties, nameof(properties)); |
|||
|
|||
this.properties = properties; |
|||
} |
|||
|
|||
public override void Update(FieldProperties newProperties) |
|||
{ |
|||
var typedProperties = ValidateProperties(newProperties); |
|||
|
|||
properties = typedProperties; |
|||
} |
|||
|
|||
private T ValidateProperties(FieldProperties newProperties) |
|||
{ |
|||
Guard.NotNull(newProperties, nameof(newProperties)); |
|||
|
|||
if (!(newProperties is T typedProperties)) |
|||
{ |
|||
throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); |
|||
} |
|||
|
|||
return typedProperties; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// GeolocationField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class GeolocationField : Field<GeolocationFieldProperties> |
|||
{ |
|||
public GeolocationField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new GeolocationFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public GeolocationField(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// ==========================================================================
|
|||
// GeolocationFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(GeolocationField))] |
|||
public sealed class GeolocationFieldProperties : FieldProperties |
|||
{ |
|||
public GeolocationFieldEditor Editor { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// ==========================================================================
|
|||
// IFieldPropertiesVisitor.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public interface IFieldPropertiesVisitor<out T> |
|||
{ |
|||
T Visit(AssetsFieldProperties properties); |
|||
|
|||
T Visit(BooleanFieldProperties properties); |
|||
|
|||
T Visit(DateTimeFieldProperties properties); |
|||
|
|||
T Visit(GeolocationFieldProperties properties); |
|||
|
|||
T Visit(JsonFieldProperties properties); |
|||
|
|||
T Visit(NumberFieldProperties properties); |
|||
|
|||
T Visit(ReferencesFieldProperties properties); |
|||
|
|||
T Visit(StringFieldProperties properties); |
|||
|
|||
T Visit(TagsFieldProperties properties); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// ==========================================================================
|
|||
// IFieldVisitor.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public interface IFieldVisitor<out T> |
|||
{ |
|||
T Visit(AssetsField field); |
|||
|
|||
T Visit(BooleanField field); |
|||
|
|||
T Visit(DateTimeField field); |
|||
|
|||
T Visit(GeolocationField field); |
|||
|
|||
T Visit(JsonField field); |
|||
|
|||
T Visit(NumberField field); |
|||
|
|||
T Visit(ReferencesField field); |
|||
|
|||
T Visit(StringField field); |
|||
|
|||
T Visit(TagsField field); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
// ==========================================================================
|
|||
// JsonSchemaModel.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Newtonsoft.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas.Json |
|||
{ |
|||
public sealed class JsonSchemaModel |
|||
{ |
|||
[JsonProperty] |
|||
public string Name { get; set; } |
|||
|
|||
[JsonProperty] |
|||
public bool IsPublished { get; set; } |
|||
|
|||
[JsonProperty] |
|||
public SchemaProperties Properties { get; set; } |
|||
|
|||
[JsonProperty] |
|||
public List<JsonFieldModel> Fields { get; set; } |
|||
|
|||
public JsonSchemaModel() |
|||
{ |
|||
} |
|||
|
|||
public JsonSchemaModel(Schema schema) |
|||
{ |
|||
Name = schema.Name; |
|||
|
|||
Properties = schema.Properties; |
|||
|
|||
Fields = |
|||
schema.Fields?.Select(x => |
|||
new JsonFieldModel |
|||
{ |
|||
Id = x.Id, |
|||
Name = x.Name, |
|||
IsHidden = x.IsHidden, |
|||
IsLocked = x.IsLocked, |
|||
IsDisabled = x.IsDisabled, |
|||
Partitioning = x.Partitioning.Key, |
|||
Properties = x.RawProperties |
|||
}).ToList(); |
|||
|
|||
IsPublished = schema.IsPublished; |
|||
} |
|||
|
|||
public Schema ToSchema(FieldRegistry fieldRegistry) |
|||
{ |
|||
var schema = new Schema(Name); |
|||
|
|||
if (Fields != null) |
|||
{ |
|||
foreach (var fieldModel in Fields) |
|||
{ |
|||
var parititonKey = new Partitioning(fieldModel.Partitioning); |
|||
|
|||
var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties); |
|||
|
|||
if (fieldModel.IsDisabled) |
|||
{ |
|||
field.Disable(); |
|||
} |
|||
|
|||
if (fieldModel.IsLocked) |
|||
{ |
|||
field.Lock(); |
|||
} |
|||
|
|||
if (fieldModel.IsHidden) |
|||
{ |
|||
field.Hide(); |
|||
} |
|||
|
|||
schema.AddField(field); |
|||
} |
|||
} |
|||
|
|||
if (IsPublished) |
|||
{ |
|||
schema.Publish(); |
|||
} |
|||
|
|||
if (Properties != null) |
|||
{ |
|||
schema.Update(Properties); |
|||
} |
|||
|
|||
return schema; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
// ==========================================================================
|
|||
// SchemaConverter.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Newtonsoft.Json; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas.Json |
|||
{ |
|||
public sealed class SchemaConverter : JsonClassConverter<Schema> |
|||
{ |
|||
private readonly FieldRegistry fieldRegistry; |
|||
|
|||
public SchemaConverter(FieldRegistry fieldRegistry) |
|||
{ |
|||
Guard.NotNull(fieldRegistry, nameof(fieldRegistry)); |
|||
|
|||
this.fieldRegistry = fieldRegistry; |
|||
} |
|||
|
|||
protected override void WriteValue(JsonWriter writer, Schema value, JsonSerializer serializer) |
|||
{ |
|||
serializer.Serialize(writer, new JsonSchemaModel(value)); |
|||
} |
|||
|
|||
protected override Schema ReadValue(JsonReader reader, JsonSerializer serializer) |
|||
{ |
|||
return serializer.Deserialize<JsonSchemaModel>(reader).ToSchema(fieldRegistry); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// JsonField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class JsonField : Field<JsonFieldProperties> |
|||
{ |
|||
public JsonField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new JsonFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public JsonField(long id, string name, Partitioning partitioning, JsonFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// ==========================================================================
|
|||
// JsonFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(JsonField))] |
|||
public sealed class JsonFieldProperties : FieldProperties |
|||
{ |
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// ==========================================================================
|
|||
// NamedElementPropertiesBase.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public abstract class NamedElementPropertiesBase |
|||
{ |
|||
public string Label { get; set; } |
|||
|
|||
public string Hints { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// NumberField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class NumberField : Field<NumberFieldProperties> |
|||
{ |
|||
public NumberField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new NumberFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public NumberField(long id, string name, Partitioning partitioning, NumberFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// ==========================================================================
|
|||
// NumberFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(NumberField))] |
|||
public sealed class NumberFieldProperties : FieldProperties |
|||
{ |
|||
public double? MaxValue { get; set; } |
|||
|
|||
public double? MinValue { get; set; } |
|||
|
|||
public double? DefaultValue { get; set; } |
|||
|
|||
public double[] AllowedValues { get; set; } |
|||
|
|||
public NumberFieldEditor Editor { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// ReferencesField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class ReferencesField : Field<ReferencesFieldProperties> |
|||
{ |
|||
public ReferencesField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new ReferencesFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public ReferencesField(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// ReferencesFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(ReferencesField))] |
|||
public sealed class ReferencesFieldProperties : FieldProperties |
|||
{ |
|||
public int? MinItems { get; set; } |
|||
|
|||
public int? MaxItems { get; set; } |
|||
|
|||
public Guid SchemaId { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
// ==========================================================================
|
|||
// Schema.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class Schema |
|||
{ |
|||
private readonly string name; |
|||
private readonly List<Field> fieldsOrdered = new List<Field>(); |
|||
private readonly Dictionary<long, Field> fieldsById = new Dictionary<long, Field>(); |
|||
private readonly Dictionary<string, Field> fieldsByName = new Dictionary<string, Field>(); |
|||
private SchemaProperties properties = new SchemaProperties(); |
|||
private bool isPublished; |
|||
|
|||
public string Name |
|||
{ |
|||
get { return name; } |
|||
} |
|||
|
|||
public bool IsPublished |
|||
{ |
|||
get { return isPublished; } |
|||
} |
|||
|
|||
public IReadOnlyList<Field> Fields |
|||
{ |
|||
get { return fieldsOrdered; } |
|||
} |
|||
|
|||
public IReadOnlyDictionary<long, Field> FieldsById |
|||
{ |
|||
get { return fieldsById; } |
|||
} |
|||
|
|||
public IReadOnlyDictionary<string, Field> FieldsByName |
|||
{ |
|||
get { return fieldsByName; } |
|||
} |
|||
|
|||
public SchemaProperties Properties |
|||
{ |
|||
get { return properties; } |
|||
} |
|||
|
|||
public void Publish() |
|||
{ |
|||
isPublished = true; |
|||
} |
|||
|
|||
public void Unpublish() |
|||
{ |
|||
isPublished = false; |
|||
} |
|||
|
|||
public Schema(string name) |
|||
{ |
|||
Guard.NotNullOrEmpty(name, nameof(name)); |
|||
|
|||
this.name = name; |
|||
} |
|||
|
|||
public void Update(SchemaProperties newProperties) |
|||
{ |
|||
Guard.NotNull(newProperties, nameof(newProperties)); |
|||
|
|||
properties = newProperties; |
|||
} |
|||
|
|||
public void DeleteField(long fieldId) |
|||
{ |
|||
if (!fieldsById.TryGetValue(fieldId, out var field)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
fieldsById.Remove(fieldId); |
|||
fieldsByName.Remove(field.Name); |
|||
fieldsOrdered.Remove(field); |
|||
} |
|||
|
|||
public void ReorderFields(List<long> ids) |
|||
{ |
|||
Guard.NotNull(ids, nameof(ids)); |
|||
|
|||
if (ids.Count != fieldsOrdered.Count || ids.Any(x => !fieldsById.ContainsKey(x))) |
|||
{ |
|||
throw new ArgumentException("Ids must cover all fields.", nameof(ids)); |
|||
} |
|||
|
|||
var fields = fieldsOrdered.ToList(); |
|||
|
|||
fieldsOrdered.Clear(); |
|||
fieldsOrdered.AddRange(fields.OrderBy(f => ids.IndexOf(f.Id))); |
|||
} |
|||
|
|||
public void AddField(Field field) |
|||
{ |
|||
Guard.NotNull(field, nameof(field)); |
|||
|
|||
if (fieldsByName.ContainsKey(field.Name) || fieldsById.ContainsKey(field.Id)) |
|||
{ |
|||
throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); |
|||
} |
|||
|
|||
fieldsById.Add(field.Id, field); |
|||
fieldsByName.Add(field.Name, field); |
|||
fieldsOrdered.Add(field); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// StringField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class StringField : Field<StringFieldProperties> |
|||
{ |
|||
public StringField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new StringFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public StringField(long id, string name, Partitioning partitioning, StringFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
// ==========================================================================
|
|||
// StringFieldProperties.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(StringField))] |
|||
public sealed class StringFieldProperties : FieldProperties |
|||
{ |
|||
public int? MinLength { get; set; } |
|||
|
|||
public int? MaxLength { get; set; } |
|||
|
|||
public string DefaultValue { get; set; } |
|||
|
|||
public string Pattern { get; set; } |
|||
|
|||
public string PatternMessage { get; set; } |
|||
|
|||
public string[] AllowedValues { get; set; } |
|||
|
|||
public StringFieldEditor Editor { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// TagsField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
public sealed class TagsField : Field<TagsFieldProperties> |
|||
{ |
|||
public TagsField(long id, string name, Partitioning partitioning) |
|||
: base(id, name, partitioning, new TagsFieldProperties()) |
|||
{ |
|||
} |
|||
|
|||
public TagsField(long id, string name, Partitioning partitioning, TagsFieldProperties properties) |
|||
: base(id, name, partitioning, properties) |
|||
{ |
|||
} |
|||
|
|||
public override T Accept<T>(IFieldVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// TagsField.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Schemas |
|||
{ |
|||
[TypeName(nameof(TagsField))] |
|||
public sealed class TagsFieldProperties : FieldProperties |
|||
{ |
|||
public int? MinItems { get; set; } |
|||
|
|||
public int? MaxItems { get; set; } |
|||
|
|||
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) |
|||
{ |
|||
return visitor.Visit(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> |
|||
<DebugType>full</DebugType> |
|||
<DebugSymbols>True</DebugSymbols> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" /> |
|||
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> |
|||
</ItemGroup> |
|||
<PropertyGroup> |
|||
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> |
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,202 @@ |
|||
// ==========================================================================
|
|||
// ContentConverter.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ConvertContent |
|||
{ |
|||
public static class ContentConverter |
|||
{ |
|||
public static NamedContentData ToNameModel(this IdContentData source, Schema schema, bool decodeJsonField) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
|
|||
var result = new NamedContentData(); |
|||
|
|||
foreach (var fieldValue in source) |
|||
{ |
|||
if (!schema.FieldsById.TryGetValue(fieldValue.Key, out var field)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (decodeJsonField && field is JsonField) |
|||
{ |
|||
var encodedValue = new ContentFieldData(); |
|||
|
|||
foreach (var partitionValue in fieldValue.Value) |
|||
{ |
|||
if (partitionValue.Value.IsNull()) |
|||
{ |
|||
encodedValue[partitionValue.Key] = null; |
|||
} |
|||
else |
|||
{ |
|||
var value = Encoding.UTF8.GetString(Convert.FromBase64String(partitionValue.Value.ToString())); |
|||
|
|||
encodedValue[partitionValue.Key] = JToken.Parse(value); |
|||
} |
|||
} |
|||
|
|||
result[field.Name] = encodedValue; |
|||
} |
|||
else |
|||
{ |
|||
result[field.Name] = fieldValue.Value; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static IdContentData ToIdModel(this NamedContentData content, Schema schema, bool encodeJsonField) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
|
|||
var result = new IdContentData(); |
|||
|
|||
foreach (var fieldValue in content) |
|||
{ |
|||
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var fieldId = field.Id; |
|||
|
|||
if (encodeJsonField && field is JsonField) |
|||
{ |
|||
var encodedValue = new ContentFieldData(); |
|||
|
|||
foreach (var partitionValue in fieldValue.Value) |
|||
{ |
|||
if (partitionValue.Value.IsNull()) |
|||
{ |
|||
encodedValue[partitionValue.Key] = null; |
|||
} |
|||
else |
|||
{ |
|||
var value = Convert.ToBase64String(Encoding.UTF8.GetBytes(partitionValue.Value.ToString())); |
|||
|
|||
encodedValue[partitionValue.Key] = value; |
|||
} |
|||
} |
|||
|
|||
result[fieldId] = encodedValue; |
|||
} |
|||
else |
|||
{ |
|||
result[fieldId] = fieldValue.Value; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static NamedContentData ToApiModel(this NamedContentData content, Schema schema, LanguagesConfig languagesConfig, bool excludeHidden = true) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
Guard.NotNull(languagesConfig, nameof(languagesConfig)); |
|||
|
|||
var codeForInvariant = InvariantPartitioning.Instance.Master.Key; |
|||
var codeForMasterLanguage = languagesConfig.Master.Language.Iso2Code; |
|||
|
|||
var result = new NamedContentData(); |
|||
|
|||
foreach (var fieldValue in content) |
|||
{ |
|||
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field) || (excludeHidden && field.IsHidden)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var fieldResult = new ContentFieldData(); |
|||
var fieldValues = fieldValue.Value; |
|||
|
|||
if (field.Partitioning.Equals(Partitioning.Language)) |
|||
{ |
|||
foreach (var languageConfig in languagesConfig) |
|||
{ |
|||
var languageCode = languageConfig.Key; |
|||
|
|||
if (fieldValues.TryGetValue(languageCode, out var value)) |
|||
{ |
|||
fieldResult.Add(languageCode, value); |
|||
} |
|||
else if (languageConfig == languagesConfig.Master && fieldValues.TryGetValue(codeForInvariant, out value)) |
|||
{ |
|||
fieldResult.Add(languageCode, value); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (fieldValues.TryGetValue(codeForInvariant, out var value)) |
|||
{ |
|||
fieldResult.Add(codeForInvariant, value); |
|||
} |
|||
else if (fieldValues.TryGetValue(codeForMasterLanguage, out value)) |
|||
{ |
|||
fieldResult.Add(codeForInvariant, value); |
|||
} |
|||
else if (fieldValues.Count > 0) |
|||
{ |
|||
fieldResult.Add(codeForInvariant, fieldValues.Values.First()); |
|||
} |
|||
} |
|||
|
|||
result.Add(field.Name, fieldResult); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static object ToLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection<Language> languagePreferences = null) |
|||
{ |
|||
Guard.NotNull(languagesConfig, nameof(languagesConfig)); |
|||
|
|||
if (languagePreferences == null || languagePreferences.Count == 0) |
|||
{ |
|||
return content; |
|||
} |
|||
|
|||
if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) |
|||
{ |
|||
languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList(); |
|||
} |
|||
|
|||
var result = new Dictionary<string, JToken>(); |
|||
|
|||
foreach (var fieldValue in content) |
|||
{ |
|||
var fieldValues = fieldValue.Value; |
|||
|
|||
foreach (var language in languagePreferences) |
|||
{ |
|||
if (fieldValues.TryGetValue(language, out var value) && value != null) |
|||
{ |
|||
result[fieldValue.Key] = value; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
// ==========================================================================
|
|||
// ContentEnricher.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Newtonsoft.Json.Linq; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.EnrichContent |
|||
{ |
|||
public sealed class ContentEnricher |
|||
{ |
|||
private readonly Schema schema; |
|||
private readonly PartitionResolver partitionResolver; |
|||
|
|||
public ContentEnricher(Schema schema, PartitionResolver partitionResolver) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
Guard.NotNull(partitionResolver, nameof(partitionResolver)); |
|||
|
|||
this.schema = schema; |
|||
|
|||
this.partitionResolver = partitionResolver; |
|||
} |
|||
|
|||
public void Enrich(NamedContentData data) |
|||
{ |
|||
Guard.NotNull(data, nameof(data)); |
|||
|
|||
foreach (var field in schema.Fields) |
|||
{ |
|||
var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); |
|||
var fieldPartition = partitionResolver(field.Partitioning); |
|||
|
|||
foreach (var partitionItem in fieldPartition) |
|||
{ |
|||
Enrich(field, fieldData, partitionItem); |
|||
} |
|||
|
|||
if (fieldData.Count > 0) |
|||
{ |
|||
data[field.Name] = fieldData; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void Enrich(Field field, ContentFieldData fieldData, IFieldPartitionItem partitionItem) |
|||
{ |
|||
Guard.NotNull(fieldData, nameof(fieldData)); |
|||
|
|||
var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); |
|||
|
|||
if (field.RawProperties.IsRequired || defaultValue.IsNull()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var key = partitionItem.Key; |
|||
|
|||
if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value)) |
|||
{ |
|||
fieldData.AddValue(key, defaultValue); |
|||
} |
|||
} |
|||
|
|||
private static bool ShouldApplyDefaultValue(Field field, JToken value) |
|||
{ |
|||
return value.IsNull() || (field is StringField && value is JValue jValue && Equals(jValue.Value, string.Empty)); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
// ==========================================================================
|
|||
// ValidatorsFactory.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Newtonsoft.Json.Linq; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.EnrichContent |
|||
{ |
|||
public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JToken> |
|||
{ |
|||
private readonly Instant now; |
|||
|
|||
private DefaultValueFactory(Instant now) |
|||
{ |
|||
this.now = now; |
|||
} |
|||
|
|||
public static JToken CreateDefaultValue(Field field, Instant now) |
|||
{ |
|||
Guard.NotNull(field, nameof(field)); |
|||
|
|||
return field.RawProperties.Accept(new DefaultValueFactory(now)); |
|||
} |
|||
|
|||
public JToken Visit(AssetsFieldProperties properties) |
|||
{ |
|||
return new JArray(); |
|||
} |
|||
|
|||
public JToken Visit(BooleanFieldProperties properties) |
|||
{ |
|||
return properties.DefaultValue; |
|||
} |
|||
|
|||
public JToken Visit(GeolocationFieldProperties properties) |
|||
{ |
|||
return JValue.CreateNull(); |
|||
} |
|||
|
|||
public JToken Visit(JsonFieldProperties properties) |
|||
{ |
|||
return JValue.CreateNull(); |
|||
} |
|||
|
|||
public JToken Visit(NumberFieldProperties properties) |
|||
{ |
|||
return properties.DefaultValue; |
|||
} |
|||
|
|||
public JToken Visit(ReferencesFieldProperties properties) |
|||
{ |
|||
return new JArray(); |
|||
} |
|||
|
|||
public JToken Visit(StringFieldProperties properties) |
|||
{ |
|||
return properties.DefaultValue; |
|||
} |
|||
|
|||
public JToken Visit(TagsFieldProperties properties) |
|||
{ |
|||
return new JArray(); |
|||
} |
|||
|
|||
public JToken Visit(DateTimeFieldProperties properties) |
|||
{ |
|||
if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) |
|||
{ |
|||
return now.ToString(); |
|||
} |
|||
|
|||
if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) |
|||
{ |
|||
return now.ToString().Substring(10); |
|||
} |
|||
|
|||
return properties.DefaultValue?.ToString(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
// ==========================================================================
|
|||
// ContentReferencesExtensions.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds |
|||
{ |
|||
public static class ContentReferencesExtensions |
|||
{ |
|||
public static IdContentData ToCleanedReferences(this IdContentData source, Schema schema, ISet<Guid> deletedReferencedIds) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
Guard.NotNull(deletedReferencedIds, nameof(deletedReferencedIds)); |
|||
|
|||
var result = new IdContentData(source); |
|||
|
|||
foreach (var field in schema.Fields) |
|||
{ |
|||
var fieldData = source.GetOrDefault(field.Id); |
|||
|
|||
if (fieldData == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull()).ToList()) |
|||
{ |
|||
var newValue = field.CleanReferences(partitionValue.Value, deletedReferencedIds); |
|||
|
|||
fieldData[partitionValue.Key] = newValue; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static IEnumerable<Guid> GetReferencedIds(this IdContentData source, Schema schema) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
|
|||
var foundReferences = new HashSet<Guid>(); |
|||
|
|||
foreach (var field in schema.Fields) |
|||
{ |
|||
var fieldData = source.GetOrDefault(field.Id); |
|||
|
|||
if (fieldData == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull())) |
|||
{ |
|||
var ids = field.ExtractReferences(partitionValue.Value); |
|||
|
|||
foreach (var id in ids.Where(x => foundReferences.Add(x))) |
|||
{ |
|||
yield return id; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// ==========================================================================
|
|||
// ReferenceExtractor.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds |
|||
{ |
|||
public static class ReferencesCleaner |
|||
{ |
|||
public static JToken CleanReferences(this Field field, JToken value, ISet<Guid> oldReferences) |
|||
{ |
|||
if ((field is AssetsField || field is ReferencesField) && !value.IsNull()) |
|||
{ |
|||
switch (field) |
|||
{ |
|||
case AssetsField assetsField: |
|||
return Visit(assetsField, value, oldReferences); |
|||
|
|||
case ReferencesField referencesField: |
|||
return Visit(referencesField, value, oldReferences); |
|||
} |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
private static JToken Visit(AssetsField field, JToken value, IEnumerable<Guid> oldReferences) |
|||
{ |
|||
var oldIds = field.ExtractReferences(value).ToList(); |
|||
var newIds = oldIds.Except(oldReferences).ToList(); |
|||
|
|||
return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value; |
|||
} |
|||
|
|||
private static JToken Visit(ReferencesField field, JToken value, ICollection<Guid> oldReferences) |
|||
{ |
|||
if (oldReferences.Contains(field.Properties.SchemaId)) |
|||
{ |
|||
return new JArray(); |
|||
} |
|||
|
|||
var oldIds = field.ExtractReferences(value).ToList(); |
|||
var newIds = oldIds.Except(oldReferences).ToList(); |
|||
|
|||
return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
// ==========================================================================
|
|||
// ReferenceExtractor.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds |
|||
{ |
|||
public static class ReferencesExtractor |
|||
{ |
|||
public static IEnumerable<Guid> ExtractReferences(this Field field, JToken value) |
|||
{ |
|||
switch (field) |
|||
{ |
|||
case AssetsField assetsField: |
|||
return Visit(assetsField, value); |
|||
|
|||
case ReferencesField referencesField: |
|||
return Visit(referencesField, value); |
|||
} |
|||
|
|||
return Enumerable.Empty<Guid>(); |
|||
} |
|||
|
|||
public static IEnumerable<Guid> Visit(AssetsField field, JToken value) |
|||
{ |
|||
IEnumerable<Guid> result = null; |
|||
try |
|||
{ |
|||
result = value?.ToObject<List<Guid>>(); |
|||
} |
|||
catch |
|||
{ |
|||
result = null; |
|||
} |
|||
|
|||
return result ?? Enumerable.Empty<Guid>(); |
|||
} |
|||
|
|||
private static IEnumerable<Guid> Visit(ReferencesField field, JToken value) |
|||
{ |
|||
IEnumerable<Guid> result = null; |
|||
try |
|||
{ |
|||
result = value?.ToObject<List<Guid>>() ?? Enumerable.Empty<Guid>(); |
|||
} |
|||
catch |
|||
{ |
|||
result = Enumerable.Empty<Guid>(); |
|||
} |
|||
|
|||
return result.Union(new[] { field.Properties.SchemaId }); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
// ==========================================================================
|
|||
// EdmSchemaExtensions.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using Microsoft.OData.Edm; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.GenerateEdmSchema |
|||
{ |
|||
public static class EdmSchemaExtensions |
|||
{ |
|||
public static string EscapeEdmField(this string field) |
|||
{ |
|||
return field.Replace("-", "_"); |
|||
} |
|||
|
|||
public static string UnescapeEdmField(this string field) |
|||
{ |
|||
return field.Replace("_", "-"); |
|||
} |
|||
|
|||
public static EdmComplexType BuildEdmType(this Schema schema, PartitionResolver partitionResolver, Func<EdmComplexType, EdmComplexType> typeResolver) |
|||
{ |
|||
Guard.NotNull(typeResolver, nameof(typeResolver)); |
|||
Guard.NotNull(partitionResolver, nameof(partitionResolver)); |
|||
|
|||
var schemaName = schema.Name.ToPascalCase(); |
|||
|
|||
var edmType = new EdmComplexType("Squidex", schemaName); |
|||
|
|||
foreach (var field in schema.FieldsByName.Values.Where(x => !x.IsHidden)) |
|||
{ |
|||
var edmValueType = EdmTypeVisitor.CreateEdmType(field); |
|||
|
|||
if (edmValueType == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var partitionType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}{field.Name.ToPascalCase()}Property")); |
|||
var partition = partitionResolver(field.Partitioning); |
|||
|
|||
foreach (var partitionItem in partition) |
|||
{ |
|||
partitionType.AddStructuralProperty(partitionItem.Key, edmValueType); |
|||
} |
|||
|
|||
edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); |
|||
} |
|||
|
|||
return edmType; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
// ==========================================================================
|
|||
// EdmTypeVisitor.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.OData.Edm; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.GenerateEdmSchema |
|||
{ |
|||
public sealed class EdmTypeVisitor : IFieldVisitor<IEdmTypeReference> |
|||
{ |
|||
private static readonly EdmTypeVisitor Instance = new EdmTypeVisitor(); |
|||
|
|||
private EdmTypeVisitor() |
|||
{ |
|||
} |
|||
|
|||
public static IEdmTypeReference CreateEdmType(Field field) |
|||
{ |
|||
return field.Accept(Instance); |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(AssetsField field) |
|||
{ |
|||
return CreatePrimitive(EdmPrimitiveTypeKind.String, field); |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(BooleanField field) |
|||
{ |
|||
return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field); |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(DateTimeField field) |
|||
{ |
|||
return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field); |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(GeolocationField field) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(JsonField field) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(NumberField field) |
|||
{ |
|||
return CreatePrimitive(EdmPrimitiveTypeKind.Double, field); |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(ReferencesField field) |
|||
{ |
|||
return CreatePrimitive(EdmPrimitiveTypeKind.String, field); |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(StringField field) |
|||
{ |
|||
return CreatePrimitive(EdmPrimitiveTypeKind.String, field); |
|||
} |
|||
|
|||
public IEdmTypeReference Visit(TagsField field) |
|||
{ |
|||
return CreatePrimitive(EdmPrimitiveTypeKind.String, field); |
|||
} |
|||
|
|||
private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, Field field) |
|||
{ |
|||
return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired); |
|||
} |
|||
} |
|||
} |
|||
@ -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 }; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
// ==========================================================================
|
|||
// JsonSchemaExtensions.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using NJsonSchema; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema |
|||
{ |
|||
public static class JsonSchemaExtensions |
|||
{ |
|||
public static JsonSchema4 BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, Func<string, JsonSchema4, JsonSchema4> schemaResolver) |
|||
{ |
|||
Guard.NotNull(schemaResolver, nameof(schemaResolver)); |
|||
Guard.NotNull(partitionResolver, nameof(partitionResolver)); |
|||
|
|||
var schemaName = schema.Name.ToPascalCase(); |
|||
|
|||
var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver); |
|||
var jsonSchema = new JsonSchema4 { Type = JsonObjectType.Object }; |
|||
|
|||
foreach (var field in schema.Fields.Where(x => !x.IsHidden)) |
|||
{ |
|||
var partitionProperty = CreateProperty(field); |
|||
var partitionObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false }; |
|||
var partition = partitionResolver(field.Partitioning); |
|||
|
|||
foreach (var partitionItem in partition) |
|||
{ |
|||
var partitionItemProperty = field.Accept(jsonTypeVisitor); |
|||
|
|||
partitionItemProperty.Description = partitionItem.Name; |
|||
partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); |
|||
} |
|||
|
|||
partitionProperty.Reference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); |
|||
|
|||
jsonSchema.Properties.Add(field.Name, partitionProperty); |
|||
} |
|||
|
|||
return jsonSchema; |
|||
} |
|||
|
|||
public static JsonProperty CreateProperty(Field field) |
|||
{ |
|||
var jsonProperty = new JsonProperty { IsRequired = field.RawProperties.IsRequired, Type = JsonObjectType.Object }; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) |
|||
{ |
|||
jsonProperty.Description = field.RawProperties.Hints; |
|||
} |
|||
else |
|||
{ |
|||
jsonProperty.Description = field.Name; |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) |
|||
{ |
|||
jsonProperty.Description += $" ({field.RawProperties.Hints})."; |
|||
} |
|||
|
|||
return jsonProperty; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
// ==========================================================================
|
|||
// JsonTypeVisitor.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.ObjectModel; |
|||
using NJsonSchema; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.GenerateJsonSchema |
|||
{ |
|||
public sealed class JsonTypeVisitor : IFieldVisitor<JsonProperty> |
|||
{ |
|||
private readonly Func<string, JsonSchema4, JsonSchema4> schemaResolver; |
|||
|
|||
public JsonTypeVisitor(Func<string, JsonSchema4, JsonSchema4> schemaResolver) |
|||
{ |
|||
this.schemaResolver = schemaResolver; |
|||
} |
|||
|
|||
public JsonProperty Visit(AssetsField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
var itemSchema = schemaResolver("AssetItem", new JsonSchema4 { Type = JsonObjectType.String }); |
|||
|
|||
jsonProperty.Type = JsonObjectType.Array; |
|||
jsonProperty.Item = itemSchema; |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(BooleanField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
jsonProperty.Type = JsonObjectType.Boolean; |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(DateTimeField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
jsonProperty.Type = JsonObjectType.String; |
|||
jsonProperty.Format = JsonFormatStrings.DateTime; |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(GeolocationField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
var geolocationSchema = new JsonSchema4 |
|||
{ |
|||
AllowAdditionalProperties = false |
|||
}; |
|||
|
|||
geolocationSchema.Properties.Add("latitude", new JsonProperty |
|||
{ |
|||
Type = JsonObjectType.Number, |
|||
Minimum = -90, |
|||
Maximum = 90, |
|||
IsRequired = true |
|||
}); |
|||
|
|||
geolocationSchema.Properties.Add("longitude", new JsonProperty |
|||
{ |
|||
Type = JsonObjectType.Number, |
|||
Minimum = -180, |
|||
Maximum = 180, |
|||
IsRequired = true |
|||
}); |
|||
|
|||
var schemaReference = schemaResolver("GeolocationDto", geolocationSchema); |
|||
|
|||
jsonProperty.Type = JsonObjectType.Object; |
|||
jsonProperty.Reference = schemaReference; |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(JsonField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
jsonProperty.Type = JsonObjectType.Object; |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(NumberField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
jsonProperty.Type = JsonObjectType.Number; |
|||
|
|||
if (field.Properties.MinValue.HasValue) |
|||
{ |
|||
jsonProperty.Minimum = (decimal)field.Properties.MinValue.Value; |
|||
} |
|||
|
|||
if (field.Properties.MaxValue.HasValue) |
|||
{ |
|||
jsonProperty.Maximum = (decimal)field.Properties.MaxValue.Value; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(ReferencesField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
var itemSchema = schemaResolver("ReferenceItem", new JsonSchema4 { Type = JsonObjectType.String }); |
|||
|
|||
jsonProperty.Type = JsonObjectType.Array; |
|||
jsonProperty.Item = itemSchema; |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(StringField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
jsonProperty.Type = JsonObjectType.String; |
|||
|
|||
jsonProperty.MinLength = field.Properties.MinLength; |
|||
jsonProperty.MaxLength = field.Properties.MaxLength; |
|||
|
|||
if (field.Properties.AllowedValues != null) |
|||
{ |
|||
var names = jsonProperty.EnumerationNames = jsonProperty.EnumerationNames ?? new Collection<string>(); |
|||
|
|||
foreach (var value in field.Properties.AllowedValues) |
|||
{ |
|||
names.Add(value); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public JsonProperty Visit(TagsField field) |
|||
{ |
|||
return CreateProperty(field, jsonProperty => |
|||
{ |
|||
var itemSchema = schemaResolver("TagsItem", new JsonSchema4 { Type = JsonObjectType.String }); |
|||
|
|||
jsonProperty.Type = JsonObjectType.Array; |
|||
jsonProperty.Item = itemSchema; |
|||
}); |
|||
} |
|||
|
|||
private static JsonProperty CreateProperty(Field field, Action<JsonProperty> updater) |
|||
{ |
|||
var property = new JsonProperty { IsRequired = field.RawProperties.IsRequired }; |
|||
|
|||
updater(property); |
|||
|
|||
return property; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
// ==========================================================================
|
|||
// ContentDataObject.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Jint; |
|||
using Jint.Native; |
|||
using Jint.Native.Object; |
|||
using Jint.Runtime.Descriptors; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable RECS0133 // Parameter name differs in base declaration
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper |
|||
{ |
|||
public sealed class ContentDataObject : ObjectInstance |
|||
{ |
|||
private readonly NamedContentData contentData; |
|||
private HashSet<string> fieldsToDelete; |
|||
private Dictionary<string, ContentDataProperty> fieldProperties; |
|||
private bool isChanged; |
|||
|
|||
public ContentDataObject(Engine engine, NamedContentData contentData) |
|||
: base(engine) |
|||
{ |
|||
Extensible = true; |
|||
|
|||
this.contentData = contentData; |
|||
} |
|||
|
|||
public void MarkChanged() |
|||
{ |
|||
isChanged = true; |
|||
} |
|||
|
|||
public bool TryUpdate(out NamedContentData result) |
|||
{ |
|||
result = contentData; |
|||
|
|||
if (isChanged) |
|||
{ |
|||
if (fieldsToDelete != null) |
|||
{ |
|||
foreach (var field in fieldsToDelete) |
|||
{ |
|||
contentData.Remove(field); |
|||
} |
|||
} |
|||
|
|||
if (fieldProperties != null) |
|||
{ |
|||
foreach (var kvp in fieldProperties) |
|||
{ |
|||
if (kvp.Value.ContentField.TryUpdate(out var fieldData)) |
|||
{ |
|||
contentData[kvp.Key] = fieldData; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return isChanged; |
|||
} |
|||
|
|||
public override void RemoveOwnProperty(string propertyName) |
|||
{ |
|||
if (fieldsToDelete == null) |
|||
{ |
|||
fieldsToDelete = new HashSet<string>(); |
|||
} |
|||
|
|||
fieldsToDelete.Add(propertyName); |
|||
fieldProperties?.Remove(propertyName); |
|||
|
|||
MarkChanged(); |
|||
} |
|||
|
|||
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
if (!fieldProperties.ContainsKey(propertyName)) |
|||
{ |
|||
fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public override void Put(string propertyName, JsValue value, bool throwOnError) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this)).Value = value; |
|||
} |
|||
|
|||
public override PropertyDescriptor GetOwnProperty(string propertyName) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
return fieldProperties.GetOrDefault(propertyName) ?? new PropertyDescriptor(new ObjectInstance(Engine) { Extensible = true }, true, false, true); |
|||
} |
|||
|
|||
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties() |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
foreach (var property in fieldProperties) |
|||
{ |
|||
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value); |
|||
} |
|||
} |
|||
|
|||
private void EnsurePropertiesInitialized() |
|||
{ |
|||
if (fieldProperties == null) |
|||
{ |
|||
fieldProperties = new Dictionary<string, ContentDataProperty>(contentData.Count); |
|||
|
|||
foreach (var kvp in contentData) |
|||
{ |
|||
fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
// ==========================================================================
|
|||
// ContentFieldObject.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Jint.Native.Object; |
|||
using Jint.Runtime.Descriptors; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable RECS0133 // Parameter name differs in base declaration
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper |
|||
{ |
|||
public sealed class ContentFieldObject : ObjectInstance |
|||
{ |
|||
private readonly ContentDataObject contentData; |
|||
private readonly ContentFieldData fieldData; |
|||
private HashSet<string> valuesToDelete; |
|||
private Dictionary<string, ContentFieldProperty> valueProperties; |
|||
private bool isChanged; |
|||
|
|||
public ContentFieldData FieldData |
|||
{ |
|||
get { return fieldData; } |
|||
} |
|||
|
|||
public ContentFieldObject(ContentDataObject contentData, ContentFieldData fieldData, bool isNew) |
|||
: base(contentData.Engine) |
|||
{ |
|||
Extensible = true; |
|||
|
|||
this.contentData = contentData; |
|||
this.fieldData = fieldData; |
|||
|
|||
if (isNew) |
|||
{ |
|||
MarkChanged(); |
|||
} |
|||
} |
|||
|
|||
public void MarkChanged() |
|||
{ |
|||
isChanged = true; |
|||
|
|||
contentData.MarkChanged(); |
|||
} |
|||
|
|||
public bool TryUpdate(out ContentFieldData result) |
|||
{ |
|||
result = fieldData; |
|||
|
|||
if (isChanged) |
|||
{ |
|||
if (valuesToDelete != null) |
|||
{ |
|||
foreach (var field in valuesToDelete) |
|||
{ |
|||
fieldData.Remove(field); |
|||
} |
|||
} |
|||
|
|||
if (valueProperties != null) |
|||
{ |
|||
foreach (var kvp in valueProperties) |
|||
{ |
|||
if (kvp.Value.IsChanged) |
|||
{ |
|||
fieldData[kvp.Key] = kvp.Value.ContentValue; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return isChanged; |
|||
} |
|||
|
|||
public override void RemoveOwnProperty(string propertyName) |
|||
{ |
|||
if (valuesToDelete == null) |
|||
{ |
|||
valuesToDelete = new HashSet<string>(); |
|||
} |
|||
|
|||
valuesToDelete.Add(propertyName); |
|||
valueProperties?.Remove(propertyName); |
|||
|
|||
MarkChanged(); |
|||
} |
|||
|
|||
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
if (!valueProperties.ContainsKey(propertyName)) |
|||
{ |
|||
valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public override PropertyDescriptor GetOwnProperty(string propertyName) |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; |
|||
} |
|||
|
|||
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties() |
|||
{ |
|||
EnsurePropertiesInitialized(); |
|||
|
|||
foreach (var property in valueProperties) |
|||
{ |
|||
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value); |
|||
} |
|||
} |
|||
|
|||
private void EnsurePropertiesInitialized() |
|||
{ |
|||
if (valueProperties == null) |
|||
{ |
|||
valueProperties = new Dictionary<string, ContentFieldProperty>(FieldData.Count); |
|||
|
|||
foreach (var kvp in FieldData) |
|||
{ |
|||
valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
// ==========================================================================
|
|||
// JintScriptEngine.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Jint; |
|||
using Jint.Native.Object; |
|||
using Jint.Parser; |
|||
using Jint.Runtime; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Scripting |
|||
{ |
|||
public sealed class JintScriptEngine : IScriptEngine |
|||
{ |
|||
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); |
|||
|
|||
public void Execute(ScriptContext context, string script) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
if (!string.IsNullOrWhiteSpace(script)) |
|||
{ |
|||
var engine = CreateScriptEngine(context); |
|||
|
|||
EnableDisallow(engine); |
|||
EnableReject(engine); |
|||
|
|||
Execute(engine, script); |
|||
} |
|||
} |
|||
|
|||
public NamedContentData ExecuteAndTransform(ScriptContext context, string script) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
var result = context.Data; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(script)) |
|||
{ |
|||
var engine = CreateScriptEngine(context); |
|||
|
|||
EnableDisallow(engine); |
|||
EnableReject(engine); |
|||
|
|||
engine.SetValue("operation", new Action(() => |
|||
{ |
|||
var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); |
|||
|
|||
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) |
|||
{ |
|||
data.TryUpdate(out result); |
|||
} |
|||
})); |
|||
|
|||
engine.SetValue("replace", new Action(() => |
|||
{ |
|||
var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); |
|||
|
|||
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) |
|||
{ |
|||
data.TryUpdate(out result); |
|||
} |
|||
})); |
|||
|
|||
Execute(engine, script); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public NamedContentData Transform(ScriptContext context, string script) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
var result = context.Data; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(script)) |
|||
{ |
|||
try |
|||
{ |
|||
var engine = CreateScriptEngine(context); |
|||
|
|||
engine.SetValue("replace", new Action(() => |
|||
{ |
|||
var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); |
|||
|
|||
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) |
|||
{ |
|||
data.TryUpdate(out result); |
|||
} |
|||
})); |
|||
|
|||
engine.Execute(script); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
result = context.Data; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static void Execute(Engine engine, string script) |
|||
{ |
|||
try |
|||
{ |
|||
engine.Execute(script); |
|||
} |
|||
catch (ParserException ex) |
|||
{ |
|||
throw new ValidationException($"Failed to execute script with javascript syntaxs error.", new ValidationError(ex.Message)); |
|||
} |
|||
catch (JavaScriptException ex) |
|||
{ |
|||
throw new ValidationException($"Failed to execute script with javascript error.", new ValidationError(ex.Message)); |
|||
} |
|||
} |
|||
|
|||
private Engine CreateScriptEngine(ScriptContext context) |
|||
{ |
|||
var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict()); |
|||
|
|||
var contextInstance = new ObjectInstance(engine); |
|||
|
|||
if (context.Data != null) |
|||
{ |
|||
contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); |
|||
} |
|||
|
|||
if (context.OldData != null) |
|||
{ |
|||
contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.OldData), true, true, true); |
|||
} |
|||
|
|||
if (context.User != null) |
|||
{ |
|||
contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false); |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(context.Operation)) |
|||
{ |
|||
contextInstance.FastAddProperty("operation", context.Operation, false, true, false); |
|||
} |
|||
|
|||
engine.SetValue("ctx", contextInstance); |
|||
|
|||
return engine; |
|||
} |
|||
|
|||
private static void EnableDisallow(Engine engine) |
|||
{ |
|||
engine.SetValue("disallow", new Action<string>(message => |
|||
{ |
|||
var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; |
|||
|
|||
throw new DomainForbiddenException(exMessage); |
|||
})); |
|||
} |
|||
|
|||
private static void EnableReject(Engine engine) |
|||
{ |
|||
engine.SetValue("reject", new Action<string>(message => |
|||
{ |
|||
var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; |
|||
|
|||
throw new ValidationException($"Script rejected the operation.", errors); |
|||
})); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace>Squidex.Domain.Apps.Core</RootNamespace> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> |
|||
<DebugType>full</DebugType> |
|||
<DebugSymbols>True</DebugSymbols> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" /> |
|||
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<PackageReference Include="Jint" Version="2.11.23" /> |
|||
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" /> |
|||
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> |
|||
<PackageReference Include="NJsonSchema" Version="9.8.3" /> |
|||
<PackageReference Include="NodaTime" Version="2.2.1" /> |
|||
<PackageReference Include="RefactoringEssentials" Version="5.2.0" /> |
|||
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> |
|||
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" /> |
|||
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> |
|||
</ItemGroup> |
|||
<PropertyGroup> |
|||
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> |
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -0,0 +1,43 @@ |
|||
// ==========================================================================
|
|||
// ContentExtensions.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent |
|||
{ |
|||
public static class ContentValidationExtensions |
|||
{ |
|||
public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList<ValidationError> errors) |
|||
{ |
|||
var validator = new ContentValidator(schema, partitionResolver, context); |
|||
|
|||
await validator.ValidateAsync(data); |
|||
|
|||
foreach (var error in validator.Errors) |
|||
{ |
|||
errors.Add(error); |
|||
} |
|||
} |
|||
|
|||
public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList<ValidationError> errors) |
|||
{ |
|||
var validator = new ContentValidator(schema, partitionResolver, context); |
|||
|
|||
await validator.ValidatePartialAsync(data); |
|||
|
|||
foreach (var error in validator.Errors) |
|||
{ |
|||
errors.Add(error); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
// ==========================================================================
|
|||
// ContentValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
#pragma warning disable 168
|
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent |
|||
{ |
|||
public sealed class ContentValidator |
|||
{ |
|||
private readonly Schema schema; |
|||
private readonly PartitionResolver partitionResolver; |
|||
private readonly ValidationContext context; |
|||
private readonly ConcurrentBag<ValidationError> errors = new ConcurrentBag<ValidationError>(); |
|||
|
|||
public IReadOnlyCollection<ValidationError> Errors |
|||
{ |
|||
get { return errors; } |
|||
} |
|||
|
|||
public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
Guard.NotNull(partitionResolver, nameof(partitionResolver)); |
|||
|
|||
this.schema = schema; |
|||
this.context = context; |
|||
this.partitionResolver = partitionResolver; |
|||
} |
|||
|
|||
public Task ValidatePartialAsync(NamedContentData data) |
|||
{ |
|||
Guard.NotNull(data, nameof(data)); |
|||
|
|||
var tasks = new List<Task>(); |
|||
|
|||
foreach (var fieldData in data) |
|||
{ |
|||
var fieldName = fieldData.Key; |
|||
|
|||
if (!schema.FieldsByName.TryGetValue(fieldData.Key, out var field)) |
|||
{ |
|||
errors.AddError("<FIELD> is not a known field.", fieldName); |
|||
} |
|||
else |
|||
{ |
|||
tasks.Add(ValidateFieldPartialAsync(field, fieldData.Value)); |
|||
} |
|||
} |
|||
|
|||
return Task.WhenAll(tasks); |
|||
} |
|||
|
|||
private Task ValidateFieldPartialAsync(Field field, ContentFieldData fieldData) |
|||
{ |
|||
var partitioning = field.Partitioning; |
|||
var partition = partitionResolver(partitioning); |
|||
|
|||
var tasks = new List<Task>(); |
|||
|
|||
foreach (var partitionValues in fieldData) |
|||
{ |
|||
if (partition.TryGetItem(partitionValues.Key, out var item)) |
|||
{ |
|||
tasks.Add(field.ValidateAsync(partitionValues.Value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item))); |
|||
} |
|||
else |
|||
{ |
|||
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field); |
|||
} |
|||
} |
|||
|
|||
return Task.WhenAll(tasks); |
|||
} |
|||
|
|||
public Task ValidateAsync(NamedContentData data) |
|||
{ |
|||
Guard.NotNull(data, nameof(data)); |
|||
|
|||
ValidateUnknownFields(data); |
|||
|
|||
var tasks = new List<Task>(); |
|||
|
|||
foreach (var field in schema.FieldsByName.Values) |
|||
{ |
|||
var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); |
|||
|
|||
tasks.Add(ValidateFieldAsync(field, fieldData)); |
|||
} |
|||
|
|||
return Task.WhenAll(tasks); |
|||
} |
|||
|
|||
private void ValidateUnknownFields(NamedContentData data) |
|||
{ |
|||
foreach (var fieldData in data) |
|||
{ |
|||
if (!schema.FieldsByName.ContainsKey(fieldData.Key)) |
|||
{ |
|||
errors.AddError("<FIELD> is not a known field.", fieldData.Key); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Task ValidateFieldAsync(Field field, ContentFieldData fieldData) |
|||
{ |
|||
var partitioning = field.Partitioning; |
|||
var partition = partitionResolver(partitioning); |
|||
|
|||
var tasks = new List<Task>(); |
|||
|
|||
foreach (var partitionValues in fieldData) |
|||
{ |
|||
if (!partition.TryGetItem(partitionValues.Key, out var _)) |
|||
{ |
|||
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field); |
|||
} |
|||
} |
|||
|
|||
foreach (var item in partition) |
|||
{ |
|||
var value = fieldData.GetOrCreate(item.Key, k => JValue.CreateNull()); |
|||
|
|||
tasks.Add(field.ValidateAsync(value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item))); |
|||
} |
|||
|
|||
return Task.WhenAll(tasks); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// ==========================================================================
|
|||
// FieldExtensions.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Json; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent |
|||
{ |
|||
public static class FieldExtensions |
|||
{ |
|||
public static void AddError(this ConcurrentBag<ValidationError> errors, string message, Field field, IFieldPartitionItem partitionItem = null) |
|||
{ |
|||
AddError(errors, message, !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name, field.Name, partitionItem); |
|||
} |
|||
|
|||
public static void AddError(this ConcurrentBag<ValidationError> errors, string message, string fieldName, IFieldPartitionItem partitionItem = null) |
|||
{ |
|||
AddError(errors, message, fieldName, fieldName, partitionItem); |
|||
} |
|||
|
|||
public static void AddError(this ConcurrentBag<ValidationError> errors, string message, string displayName, string fieldName, IFieldPartitionItem partitionItem = null) |
|||
{ |
|||
if (partitionItem != null && partitionItem != InvariantPartitioning.Instance.Master) |
|||
{ |
|||
displayName += $" ({partitionItem.Key})"; |
|||
} |
|||
|
|||
errors.Add(new ValidationError(message.Replace("<FIELD>", displayName), fieldName)); |
|||
} |
|||
|
|||
public static async Task ValidateAsync(this Field field, JToken value, ValidationContext context, Action<string> addError) |
|||
{ |
|||
try |
|||
{ |
|||
var typedValue = value.IsNull() ? null : JsonValueConverter.ConvertValue(field, value); |
|||
|
|||
foreach (var validator in ValidatorsFactory.CreateValidators(field)) |
|||
{ |
|||
await validator.ValidateAsync(typedValue, context, addError); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
addError("<FIELD> is not a valid value."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
// ==========================================================================
|
|||
// JsonValueConverter.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Newtonsoft.Json.Linq; |
|||
using NodaTime.Text; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent |
|||
{ |
|||
public sealed class JsonValueConverter : IFieldVisitor<object> |
|||
{ |
|||
public JToken Value { get; } |
|||
|
|||
private JsonValueConverter(JToken value) |
|||
{ |
|||
this.Value = value; |
|||
} |
|||
|
|||
public static object ConvertValue(Field field, JToken json) |
|||
{ |
|||
return field.Accept(new JsonValueConverter(json)); |
|||
} |
|||
|
|||
public object Visit(AssetsField field) |
|||
{ |
|||
return Value.ToObject<List<Guid>>(); |
|||
} |
|||
|
|||
public object Visit(BooleanField field) |
|||
{ |
|||
return (bool?)Value; |
|||
} |
|||
|
|||
public object Visit(DateTimeField field) |
|||
{ |
|||
if (Value.Type == JTokenType.String) |
|||
{ |
|||
var parseResult = InstantPattern.General.Parse(Value.ToString()); |
|||
|
|||
if (!parseResult.Success) |
|||
{ |
|||
throw parseResult.Exception; |
|||
} |
|||
|
|||
return parseResult.Value; |
|||
} |
|||
|
|||
throw new InvalidCastException("Invalid json type, expected string."); |
|||
} |
|||
|
|||
public object Visit(GeolocationField field) |
|||
{ |
|||
var geolocation = (JObject)Value; |
|||
|
|||
foreach (var property in geolocation.Properties()) |
|||
{ |
|||
if (!string.Equals(property.Name, "latitude", StringComparison.OrdinalIgnoreCase) && |
|||
!string.Equals(property.Name, "longitude", StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
throw new InvalidCastException("Geolocation can only have latitude and longitude property."); |
|||
} |
|||
} |
|||
|
|||
var lat = (double)geolocation["latitude"]; |
|||
var lon = (double)geolocation["longitude"]; |
|||
|
|||
if (!lat.IsBetween(-90, 90)) |
|||
{ |
|||
throw new InvalidCastException("Latitude must be between -90 and 90."); |
|||
} |
|||
|
|||
if (!lon.IsBetween(-180, 180)) |
|||
{ |
|||
throw new InvalidCastException("Longitude must be between -180 and 180."); |
|||
} |
|||
|
|||
return Value; |
|||
} |
|||
|
|||
public object Visit(JsonField field) |
|||
{ |
|||
return Value; |
|||
} |
|||
|
|||
public object Visit(NumberField field) |
|||
{ |
|||
return (double?)Value; |
|||
} |
|||
|
|||
public object Visit(ReferencesField field) |
|||
{ |
|||
return Value.ToObject<List<Guid>>(); |
|||
} |
|||
|
|||
public object Visit(StringField field) |
|||
{ |
|||
return Value.ToString(); |
|||
} |
|||
|
|||
public object Visit(TagsField field) |
|||
{ |
|||
return Value.ToObject<List<string>>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
// ==========================================================================
|
|||
// ValidationContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent |
|||
{ |
|||
public sealed class ValidationContext |
|||
{ |
|||
private readonly Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent; |
|||
private readonly Func<IEnumerable<Guid>, Task<IReadOnlyList<Guid>>> checkAsset; |
|||
|
|||
public bool IsOptional { get; } |
|||
|
|||
public ValidationContext( |
|||
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent, |
|||
Func<IEnumerable<Guid>, Task<IReadOnlyList<Guid>>> checkAsset) |
|||
: this(checkContent, checkAsset, false) |
|||
{ |
|||
} |
|||
|
|||
private ValidationContext( |
|||
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent, |
|||
Func<IEnumerable<Guid>, Task<IReadOnlyList<Guid>>> checkAsset, |
|||
bool isOptional) |
|||
{ |
|||
Guard.NotNull(checkAsset, nameof(checkAsset)); |
|||
Guard.NotNull(checkContent, nameof(checkAsset)); |
|||
|
|||
this.checkContent = checkContent; |
|||
this.checkAsset = checkAsset; |
|||
|
|||
IsOptional = isOptional; |
|||
} |
|||
|
|||
public ValidationContext Optional(bool isOptional) |
|||
{ |
|||
return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, isOptional); |
|||
} |
|||
|
|||
public Task<IReadOnlyList<Guid>> GetInvalidContentIdsAsync(IEnumerable<Guid> contentIds, Guid schemaId) |
|||
{ |
|||
return checkContent(contentIds, schemaId); |
|||
} |
|||
|
|||
public Task<IReadOnlyList<Guid>> GetInvalidAssetIdsAsync(IEnumerable<Guid> assetId) |
|||
{ |
|||
return checkAsset(assetId); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
// ==========================================================================
|
|||
// AllowedValuesValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators |
|||
{ |
|||
public sealed class AllowedValuesValidator<T> : IValidator |
|||
{ |
|||
private readonly T[] allowedValues; |
|||
|
|||
public AllowedValuesValidator(params T[] allowedValues) |
|||
{ |
|||
Guard.NotNull(allowedValues, nameof(allowedValues)); |
|||
|
|||
this.allowedValues = allowedValues; |
|||
} |
|||
|
|||
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError) |
|||
{ |
|||
if (value == null) |
|||
{ |
|||
return TaskHelper.Done; |
|||
} |
|||
|
|||
var typedValue = (T)value; |
|||
|
|||
if (!allowedValues.Contains(typedValue)) |
|||
{ |
|||
addError("<FIELD> is not an allowed value."); |
|||
} |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// ==========================================================================
|
|||
// AssetsValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators |
|||
{ |
|||
public sealed class AssetsValidator : IValidator |
|||
{ |
|||
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError) |
|||
{ |
|||
if (value is ICollection<Guid> assetIds) |
|||
{ |
|||
var invalidIds = await context.GetInvalidAssetIdsAsync(assetIds); |
|||
|
|||
foreach (var invalidId in invalidIds) |
|||
{ |
|||
addError($"<FIELD> contains invalid asset '{invalidId}'."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
// ==========================================================================
|
|||
// CollectionItemValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators |
|||
{ |
|||
public sealed class CollectionItemValidator<T> : IValidator |
|||
{ |
|||
private readonly IValidator[] itemValidators; |
|||
|
|||
public CollectionItemValidator(params IValidator[] itemValidators) |
|||
{ |
|||
Guard.NotNull(itemValidators, nameof(itemValidators)); |
|||
Guard.NotEmpty(itemValidators, nameof(itemValidators)); |
|||
|
|||
this.itemValidators = itemValidators; |
|||
} |
|||
|
|||
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError) |
|||
{ |
|||
if (value is ICollection<T> items) |
|||
{ |
|||
var innerContext = context.Optional(false); |
|||
|
|||
var index = 1; |
|||
|
|||
foreach (var item in items) |
|||
{ |
|||
foreach (var itemValidator in itemValidators) |
|||
{ |
|||
await itemValidator.ValidateAsync(item, innerContext, e => addError(e.Replace("<FIELD>", $"<FIELD> item #{index}"))); |
|||
} |
|||
|
|||
index++; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// ==========================================================================
|
|||
// CollectionValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators |
|||
{ |
|||
public sealed class CollectionValidator<T> : IValidator |
|||
{ |
|||
private readonly bool isRequired; |
|||
private readonly int? minItems; |
|||
private readonly int? maxItems; |
|||
|
|||
public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) |
|||
{ |
|||
this.isRequired = isRequired; |
|||
this.minItems = minItems; |
|||
this.maxItems = maxItems; |
|||
} |
|||
|
|||
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError) |
|||
{ |
|||
if (!(value is ICollection<T> items) || items.Count == 0) |
|||
{ |
|||
if (isRequired && !context.IsOptional) |
|||
{ |
|||
addError("<FIELD> is required."); |
|||
} |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
|
|||
if (minItems.HasValue && items.Count < minItems.Value) |
|||
{ |
|||
addError($"<FIELD> must have at least {minItems} item(s)."); |
|||
} |
|||
|
|||
if (maxItems.HasValue && items.Count > maxItems.Value) |
|||
{ |
|||
addError($"<FIELD> must have not more than {maxItems} item(s)."); |
|||
} |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// IValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators |
|||
{ |
|||
public interface IValidator |
|||
{ |
|||
Task ValidateAsync(object value, ValidationContext context, Action<string> addError); |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
// ==========================================================================
|
|||
// PatternValidator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Text.RegularExpressions; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators |
|||
{ |
|||
public class PatternValidator : IValidator |
|||
{ |
|||
private readonly Regex regex; |
|||
private readonly string errorMessage; |
|||
|
|||
public PatternValidator(string pattern, string errorMessage = null) |
|||
{ |
|||
this.errorMessage = errorMessage; |
|||
|
|||
regex = new Regex("^" + pattern + "$"); |
|||
} |
|||
|
|||
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError) |
|||
{ |
|||
if (value is string stringValue) |
|||
{ |
|||
if (!string.IsNullOrEmpty(stringValue) && !regex.IsMatch(stringValue)) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(errorMessage)) |
|||
{ |
|||
addError("<FIELD> is not valid."); |
|||
} |
|||
else |
|||
{ |
|||
addError(errorMessage); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue