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
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex Group
|
// Copyright (c) Squidex Group
|
||||
// All rights reserved.
|
// 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 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