Browse Source

Merge pull request #297 from Squidex/nested-schemas

Nested schemas
pull/303/head
Sebastian Stehle 8 years ago
committed by GitHub
parent
commit
3e190a3a17
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .gitignore
  2. 6
      src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
  3. 4
      src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs
  4. 4
      src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  5. 7
      src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
  6. 77
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  7. 40
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
  8. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  9. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  10. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
  11. 161
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  12. 158
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
  13. 4
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  14. 31
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
  15. 160
      src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
  16. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
  17. 20
      src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs
  18. 4
      src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs
  19. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
  20. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
  21. 13
      src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs
  22. 14
      src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs
  23. 14
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
  24. 29
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs
  25. 53
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  26. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
  27. 106
      src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs
  28. 70
      src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
  29. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  30. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  31. 24
      src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs
  32. 22
      src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
  33. 164
      src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  34. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  35. 9
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
  36. 122
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs
  37. 147
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  38. 16
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs
  39. 81
      src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs
  40. 39
      src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs
  41. 96
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  42. 69
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs
  43. 101
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs
  44. 15
      src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs
  45. 5
      src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs
  46. 25
      src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  47. 4
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  48. 99
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  49. 57
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs
  50. 31
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  51. 52
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs
  52. 19
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs
  53. 5
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs
  54. 35
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  55. 17
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs
  56. 15
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs
  57. 53
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs
  58. 6
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs
  59. 68
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs
  60. 8
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs
  61. 6
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs
  62. 4
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs
  63. 5
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs
  64. 5
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs
  65. 6
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs
  66. 96
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs
  67. 14
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs
  68. 8
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  69. 4
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  70. 6
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  71. 14
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs
  72. 6
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  73. 7
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  74. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  75. 34
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  76. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs
  77. 5
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  78. 44
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  79. 344
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
  80. 44
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/CommandVersionGraphType.cs
  81. 44
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataChangedResultGraphType.cs
  82. 74
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphInputType.cs
  83. 32
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  84. 31
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GeolocationInputGraphType.cs
  85. 66
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs
  86. 61
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  87. 66
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  88. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType.cs
  89. 32
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs
  90. 42
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs
  91. 25
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs
  92. 9
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs
  93. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs
  94. 3
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs
  95. 22
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaNestedField.cs
  96. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs
  97. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs
  98. 13
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs
  99. 89
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  100. 122
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs

6
.gitignore

@ -21,7 +21,9 @@ node_modules/
# Scripts (should be copied from node_modules on build)
**/wwwroot/scripts/**/*.*
/src/Squidex/appsettings.Development.json
/src/Squidex/Assets
/src/Squidex/package-lock.json
/src/Squidex/Properties/launchSettings.json
/src/Squidex/appsettings.Development.json
/src/Squidex/Properties/launchSettings.json
/global.json

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

@ -25,8 +25,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
protected ContentData(IDictionary<T, ContentFieldData> copy, IEqualityComparer<T> comparer)
: base(copy, comparer)
protected ContentData(int capacity, IEqualityComparer<T> comparer)
: base(capacity, comparer)
{
}
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
foreach (var otherValue in source)
{
var fieldValue = target.GetOrAdd(otherValue.Key, x => new ContentFieldData());
var fieldValue = target.GetOrAddNew(otherValue.Key);
foreach (var value in otherValue.Value)
{

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

@ -18,8 +18,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
public IdContentData(IdContentData copy)
: base(copy, EqualityComparer<long>.Default)
public IdContentData(int capacity)
: base(capacity, EqualityComparer<long>.Default)
{
}

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

@ -18,8 +18,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
public NamedContentData(NamedContentData copy)
: base(copy, EqualityComparer<string>.Default)
public NamedContentData(int capacity)
: base(capacity, EqualityComparer<string>.Default)
{
}

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

@ -45,5 +45,12 @@ namespace Squidex.Domain.Apps.Core
{
return Key;
}
public static Partitioning FromString(string value)
{
var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase);
return isLanguage ? Language : Invariant;
}
}
}

77
src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs

@ -0,0 +1,77 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class ArrayField : RootField<ArrayFieldProperties>, IArrayField
{
private FieldCollection<NestedField> fields = FieldCollection<NestedField>.Empty;
public IReadOnlyList<NestedField> Fields
{
get { return fields.Ordered; }
}
public IReadOnlyDictionary<long, NestedField> FieldsById
{
get { return fields.ById; }
}
public IReadOnlyDictionary<string, NestedField> FieldsByName
{
get { return fields.ByName; }
}
public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties)
: base(id, name, partitioning, properties)
{
}
[Pure]
public ArrayField DeleteField(long fieldId)
{
return Updatefields(f => f.Remove(fieldId));
}
[Pure]
public ArrayField ReorderFields(List<long> ids)
{
return Updatefields(f => f.Reorder(ids));
}
[Pure]
public ArrayField AddField(NestedField field)
{
return Updatefields(f => f.Add(field));
}
[Pure]
public ArrayField UpdateField(long fieldId, Func<NestedField, NestedField> updater)
{
return Updatefields(f => f.Update(fieldId, updater));
}
private ArrayField Updatefields(Func<FieldCollection<NestedField>, FieldCollection<NestedField>> updater)
{
var newFields = updater(fields);
if (ReferenceEquals(newFields, fields))
{
return this;
}
return Clone<ArrayField>(clone =>
{
clone.fields = newFields;
});
}
}
}

40
src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs

@ -0,0 +1,40 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
[TypeName("ArrayField")]
public sealed class ArrayFieldProperties : FieldProperties
{
public int? MinItems { get; set; }
public int? MaxItems { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override T Accept<T>(IFieldVisitor<T> visitor, IField field)
{
return visitor.Visit((IArrayField)field);
}
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return Fields.Array(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
throw new NotSupportedException();
}
}
}

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

@ -47,9 +47,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<AssetsFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<AssetsFieldProperties>(id, name, partitioning, this);
return Fields.Assets(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.Assets(id, name, this);
}
}
}

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

@ -28,9 +28,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<BooleanFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<BooleanFieldProperties>(id, name, partitioning, this);
return Fields.Boolean(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.Boolean(id, name, this);
}
}
}

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

@ -33,9 +33,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<DateTimeFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<DateTimeFieldProperties>(id, name, partitioning, this);
return Fields.DateTime(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.DateTime(id, name, this);
}
}
}

161
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs

@ -0,0 +1,161 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class FieldCollection<T> : Cloneable<FieldCollection<T>> where T : IField
{
public static readonly FieldCollection<T> Empty = new FieldCollection<T>();
private ImmutableArray<T> fieldsOrdered = ImmutableArray<T>.Empty;
private ImmutableDictionary<long, T> fieldsById;
private ImmutableDictionary<string, T> fieldsByName;
public IReadOnlyList<T> Ordered
{
get { return fieldsOrdered; }
}
public IReadOnlyDictionary<long, T> ById
{
get
{
if (fieldsById == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsById = ImmutableDictionary<long, T>.Empty;
}
else
{
fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
}
}
return fieldsById;
}
}
public IReadOnlyDictionary<string, T> ByName
{
get
{
if (fieldsByName == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsByName = ImmutableDictionary<string, T>.Empty;
}
else
{
fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name);
}
}
return fieldsByName;
}
}
private FieldCollection()
{
}
public FieldCollection(T[] fields)
{
Guard.NotNull(fields, nameof(fields));
fieldsOrdered = ImmutableArray.Create(fields);
}
protected override void OnCloned()
{
fieldsById = null;
fieldsByName = null;
}
[Pure]
public FieldCollection<T> Remove(long fieldId)
{
if (!ById.TryGetValue(fieldId, out var field))
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.Remove(field);
});
}
[Pure]
public FieldCollection<T> Reorder(List<long> ids)
{
Guard.NotNull(ids, nameof(ids));
if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x)))
{
throw new ArgumentException("Ids must cover all fields.", nameof(ids));
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToImmutableArray();
});
}
[Pure]
public FieldCollection<T> Add(T field)
{
Guard.NotNull(field, nameof(field));
if (ByName.ContainsKey(field.Name) || ById.ContainsKey(field.Id))
{
throw new ArgumentException($"A field with name '{field.Name}' and id {field.Id} already exists.", nameof(field));
}
return Clone(clone =>
{
clone.fieldsOrdered = clone.fieldsOrdered.Add(field);
});
}
[Pure]
public FieldCollection<T> Update(long fieldId, Func<T, T> updater)
{
Guard.NotNull(updater, nameof(updater));
if (!ById.TryGetValue(fieldId, out var field))
{
return this;
}
var newField = updater(field);
if (ReferenceEquals(newField, field))
{
return this;
}
if (!(newField is T typedField))
{
throw new InvalidOperationException($"Field must be of type {typeof(T)}");
}
return Clone(clone =>
{
clone.fieldsOrdered = clone.fieldsOrdered.Replace(field, typedField);
});
}
}
}

158
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs

@ -0,0 +1,158 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Schemas
{
public static class FieldExtensions
{
public static Schema ReorderFields(this Schema schema, List<long> ids, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.ReorderFields(ids);
}
return f;
});
}
return schema.ReorderFields(ids);
}
public static Schema DeleteField(this Schema schema, long fieldId, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.DeleteField(fieldId);
}
return f;
});
}
return schema.DeleteField(fieldId);
}
public static Schema LockField(this Schema schema, long fieldId, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.UpdateField(fieldId, n => n.Lock());
}
return f;
});
}
return schema.UpdateField(fieldId, f => f.Lock());
}
public static Schema HideField(this Schema schema, long fieldId, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.UpdateField(fieldId, n => n.Hide());
}
return f;
});
}
return schema.UpdateField(fieldId, f => f.Hide());
}
public static Schema ShowField(this Schema schema, long fieldId, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.UpdateField(fieldId, n => n.Show());
}
return f;
});
}
return schema.UpdateField(fieldId, f => f.Show());
}
public static Schema EnableField(this Schema schema, long fieldId, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.UpdateField(fieldId, n => n.Enable());
}
return f;
});
}
return schema.UpdateField(fieldId, f => f.Enable());
}
public static Schema DisableField(this Schema schema, long fieldId, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.UpdateField(fieldId, n => n.Disable());
}
return f;
});
}
return schema.UpdateField(fieldId, f => f.Disable());
}
public static Schema UpdateField(this Schema schema, long fieldId, FieldProperties properties, long? parentId = null)
{
if (parentId != null)
{
return schema.UpdateField(parentId.Value, f =>
{
if (f is ArrayField arrayField)
{
return arrayField.UpdateField(fieldId, n => n.Update(properties));
}
return f;
});
}
return schema.UpdateField(fieldId, f => f.Update(properties));
}
}
}

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

@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public abstract T Accept<T>(IFieldVisitor<T> visitor, IField field);
public abstract Field CreateField(long id, string name, Partitioning partitioning);
public abstract RootField CreateRootField(long id, string name, Partitioning partitioning);
public abstract NestedField CreateNestedField(long id, string name);
}
}

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

@ -14,10 +14,8 @@ 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, FactoryFunction> fieldsByPropertyType = new Dictionary<Type, FactoryFunction>();
private readonly HashSet<Type> supportedFields = new HashSet<Type>();
public FieldRegistry(TypeNameRegistry typeNameRegistry)
{
@ -38,23 +36,34 @@ namespace Squidex.Domain.Apps.Core.Schemas
private void RegisterField(Type type)
{
typeNameRegistry.Map(type);
if (supportedFields.Add(type))
{
typeNameRegistry.Map(type);
}
}
public RootField CreateRootField(long id, string name, Partitioning partitioning, FieldProperties properties)
{
CheckProperties(properties);
fieldsByPropertyType[type] = (id, name, partitioning, properties) => properties.CreateField(id, name, partitioning);
return properties.CreateRootField(id, name, partitioning);
}
public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties)
public NestedField CreateNestedField(long id, string name, FieldProperties properties)
{
Guard.NotNull(properties, nameof(properties));
CheckProperties(properties);
return properties.CreateNestedField(id, name);
}
var factory = fieldsByPropertyType.GetOrDefault(properties.GetType());
private void CheckProperties(FieldProperties properties)
{
Guard.NotNull(properties, nameof(properties));
if (factory == null)
if (!supportedFields.Contains(properties.GetType()))
{
throw new InvalidOperationException($"The field property '{properties.GetType()}' is not supported.");
}
return factory(id, name, partitioning, properties);
}
}
}

160
src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs

@ -5,53 +5,132 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Core.Schemas
{
public static class Fields
{
public static Field<AssetsFieldProperties> Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null)
public static RootField<ArrayFieldProperties> Array(long id, string name, Partitioning partitioning, params NestedField[] fields)
{
var result = new ArrayField(id, name, partitioning, new ArrayFieldProperties());
if (fields != null)
{
foreach (var field in fields)
{
result = result.AddField(field);
}
}
return result;
}
public static ArrayField Array(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null)
{
return new ArrayField(id, name, partitioning, properties ?? new ArrayFieldProperties());
}
public static RootField<AssetsFieldProperties> Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null)
{
return new RootField<AssetsFieldProperties>(id, name, partitioning, properties ?? new AssetsFieldProperties());
}
public static RootField<BooleanFieldProperties> Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null)
{
return new RootField<BooleanFieldProperties>(id, name, partitioning, properties ?? new BooleanFieldProperties());
}
public static RootField<DateTimeFieldProperties> DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null)
{
return new RootField<DateTimeFieldProperties>(id, name, partitioning, properties ?? new DateTimeFieldProperties());
}
public static RootField<GeolocationFieldProperties> Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null)
{
return new RootField<GeolocationFieldProperties>(id, name, partitioning, properties ?? new GeolocationFieldProperties());
}
public static RootField<JsonFieldProperties> Json(long id, string name, Partitioning partitioning, JsonFieldProperties properties = null)
{
return new RootField<JsonFieldProperties>(id, name, partitioning, properties ?? new JsonFieldProperties());
}
public static RootField<NumberFieldProperties> Number(long id, string name, Partitioning partitioning, NumberFieldProperties properties = null)
{
return new RootField<NumberFieldProperties>(id, name, partitioning, properties ?? new NumberFieldProperties());
}
public static RootField<ReferencesFieldProperties> References(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null)
{
return new RootField<ReferencesFieldProperties>(id, name, partitioning, properties ?? new ReferencesFieldProperties());
}
public static RootField<StringFieldProperties> String(long id, string name, Partitioning partitioning, StringFieldProperties properties = null)
{
return new RootField<StringFieldProperties>(id, name, partitioning, properties ?? new StringFieldProperties());
}
public static RootField<TagsFieldProperties> Tags(long id, string name, Partitioning partitioning, TagsFieldProperties properties = null)
{
return new RootField<TagsFieldProperties>(id, name, partitioning, properties ?? new TagsFieldProperties());
}
public static NestedField<AssetsFieldProperties> Assets(long id, string name, AssetsFieldProperties properties = null)
{
return new Field<AssetsFieldProperties>(id, name, partitioning, properties ?? new AssetsFieldProperties());
return new NestedField<AssetsFieldProperties>(id, name, properties ?? new AssetsFieldProperties());
}
public static Field<BooleanFieldProperties> Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null)
public static NestedField<BooleanFieldProperties> Boolean(long id, string name, BooleanFieldProperties properties = null)
{
return new Field<BooleanFieldProperties>(id, name, partitioning, properties ?? new BooleanFieldProperties());
return new NestedField<BooleanFieldProperties>(id, name, properties ?? new BooleanFieldProperties());
}
public static Field<DateTimeFieldProperties> DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null)
public static NestedField<DateTimeFieldProperties> DateTime(long id, string name, DateTimeFieldProperties properties = null)
{
return new Field<DateTimeFieldProperties>(id, name, partitioning, properties ?? new DateTimeFieldProperties());
return new NestedField<DateTimeFieldProperties>(id, name, properties ?? new DateTimeFieldProperties());
}
public static Field<GeolocationFieldProperties> Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null)
public static NestedField<GeolocationFieldProperties> Geolocation(long id, string name, GeolocationFieldProperties properties = null)
{
return new Field<GeolocationFieldProperties>(id, name, partitioning, properties ?? new GeolocationFieldProperties());
return new NestedField<GeolocationFieldProperties>(id, name, properties ?? new GeolocationFieldProperties());
}
public static Field<JsonFieldProperties> Json(long id, string name, Partitioning partitioning, JsonFieldProperties properties = null)
public static NestedField<JsonFieldProperties> Json(long id, string name, JsonFieldProperties properties = null)
{
return new Field<JsonFieldProperties>(id, name, partitioning, properties ?? new JsonFieldProperties());
return new NestedField<JsonFieldProperties>(id, name, properties ?? new JsonFieldProperties());
}
public static Field<NumberFieldProperties> Number(long id, string name, Partitioning partitioning, NumberFieldProperties properties = null)
public static NestedField<NumberFieldProperties> Number(long id, string name, NumberFieldProperties properties = null)
{
return new Field<NumberFieldProperties>(id, name, partitioning, properties ?? new NumberFieldProperties());
return new NestedField<NumberFieldProperties>(id, name, properties ?? new NumberFieldProperties());
}
public static Field<ReferencesFieldProperties> References(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null)
public static NestedField<ReferencesFieldProperties> References(long id, string name, ReferencesFieldProperties properties = null)
{
return new Field<ReferencesFieldProperties>(id, name, partitioning, properties ?? new ReferencesFieldProperties());
return new NestedField<ReferencesFieldProperties>(id, name, properties ?? new ReferencesFieldProperties());
}
public static Field<StringFieldProperties> String(long id, string name, Partitioning partitioning, StringFieldProperties properties = null)
public static NestedField<StringFieldProperties> String(long id, string name, StringFieldProperties properties = null)
{
return new Field<StringFieldProperties>(id, name, partitioning, properties ?? new StringFieldProperties());
return new NestedField<StringFieldProperties>(id, name, properties ?? new StringFieldProperties());
}
public static Field<TagsFieldProperties> Tags(long id, string name, Partitioning partitioning, TagsFieldProperties properties = null)
public static NestedField<TagsFieldProperties> Tags(long id, string name, TagsFieldProperties properties = null)
{
return new Field<TagsFieldProperties>(id, name, partitioning, properties ?? new TagsFieldProperties());
return new NestedField<TagsFieldProperties>(id, name, properties ?? new TagsFieldProperties());
}
public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func<ArrayField, ArrayField> handler, ArrayFieldProperties properties = null)
{
var field = Array(id, name, partitioning, properties);
if (handler != null)
{
field = handler(field);
}
return schema.AddField(field);
}
public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null)
@ -98,5 +177,50 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
return schema.AddField(Tags(id, name, partitioning, properties));
}
public static ArrayField AddAssets(this ArrayField field, long id, string name, AssetsFieldProperties properties = null)
{
return field.AddField(Assets(id, name, properties));
}
public static ArrayField AddBoolean(this ArrayField field, long id, string name, BooleanFieldProperties properties = null)
{
return field.AddField(Boolean(id, name, properties));
}
public static ArrayField AddDateTime(this ArrayField field, long id, string name, DateTimeFieldProperties properties = null)
{
return field.AddField(DateTime(id, name, properties));
}
public static ArrayField AddGeolocation(this ArrayField field, long id, string name, GeolocationFieldProperties properties = null)
{
return field.AddField(Geolocation(id, name, properties));
}
public static ArrayField AddJson(this ArrayField field, long id, string name, JsonFieldProperties properties = null)
{
return field.AddField(Json(id, name, properties));
}
public static ArrayField AddNumber(this ArrayField field, long id, string name, NumberFieldProperties properties = null)
{
return field.AddField(Number(id, name, properties));
}
public static ArrayField AddReferences(this ArrayField field, long id, string name, ReferencesFieldProperties properties = null)
{
return field.AddField(References(id, name, properties));
}
public static ArrayField AddString(this ArrayField field, long id, string name, StringFieldProperties properties = null)
{
return field.AddField(String(id, name, properties));
}
public static ArrayField AddTags(this ArrayField field, long id, string name, TagsFieldProperties properties = null)
{
return field.AddField(Tags(id, name, properties));
}
}
}

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

@ -24,9 +24,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<GeolocationFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<GeolocationFieldProperties>(id, name, partitioning, this);
return Fields.Geolocation(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.Geolocation(id, name, this);
}
}
}

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

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IArrayField : IField<ArrayFieldProperties>
{
IReadOnlyList<NestedField> Fields { get; }
IReadOnlyDictionary<long, NestedField> FieldsById { get; }
IReadOnlyDictionary<string, NestedField> FieldsByName { get; }
}
}

4
src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs

@ -13,12 +13,12 @@ namespace Squidex.Domain.Apps.Core.Schemas
string Name { get; }
bool IsLocked { get; }
bool IsDisabled { get; }
bool IsHidden { get; }
bool IsLocked { get; }
FieldProperties RawProperties { get; }
T Accept<T>(IFieldVisitor<T> visitor);

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

@ -9,6 +9,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldPropertiesVisitor<out T>
{
T Visit(ArrayFieldProperties properties);
T Visit(AssetsFieldProperties properties);
T Visit(BooleanFieldProperties properties);

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

@ -9,6 +9,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldVisitor<out T>
{
T Visit(IArrayField field);
T Visit(IField<AssetsFieldProperties> field);
T Visit(IField<BooleanFieldProperties> field);

13
src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public interface INestedField : IField
{
}
}

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

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IRootField : IField
{
Partitioning Partitioning { get; }
}
}

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json
@ -15,21 +16,24 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
public long Id { get; set; }
[JsonProperty]
public bool IsHidden { get; set; }
public string Name { get; set; }
[JsonProperty]
public bool IsLocked { get; set; }
public string Partitioning { get; set; }
[JsonProperty]
public bool IsDisabled { get; set; }
public bool IsHidden { get; set; }
[JsonProperty]
public string Name { get; set; }
public bool IsLocked { get; set; }
[JsonProperty]
public string Partitioning { get; set; }
public bool IsDisabled { get; set; }
[JsonProperty]
public FieldProperties Properties { get; set; }
[JsonProperty]
public List<JsonNestedFieldModel> Children { get; set; }
}
}

29
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json
{
public sealed class JsonNestedFieldModel
{
[JsonProperty]
public long Id { get; set; }
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public bool IsHidden { get; set; }
[JsonProperty]
public bool IsDisabled { get; set; }
[JsonProperty]
public FieldProperties Properties { get; set; }
}
}

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

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
{
public sealed class JsonSchemaModel
{
private static readonly Field[] Empty = new Field[0];
private static readonly RootField[] Empty = new RootField[0];
[JsonProperty]
public string Name { get; set; }
@ -38,11 +38,12 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
Properties = schema.Properties;
Fields =
schema.Fields?.Select(x =>
schema.Fields.Select(x =>
new JsonFieldModel
{
Id = x.Id,
Name = x.Name,
Children = CreateChildren(x),
IsHidden = x.IsHidden,
IsLocked = x.IsLocked,
IsDisabled = x.IsDisabled,
@ -53,13 +54,31 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
IsPublished = schema.IsPublished;
}
public Schema ToSchema(FieldRegistry fieldRegistry)
private static List<JsonNestedFieldModel> CreateChildren(IField field)
{
Field[] fields = Empty;
if (field is ArrayField arrayField)
{
return arrayField.Fields.Select(x =>
new JsonNestedFieldModel
{
Id = x.Id,
Name = x.Name,
IsHidden = x.IsHidden,
IsDisabled = x.IsDisabled,
Properties = x.RawProperties
}).ToList();
}
return null;
}
public Schema ToSchema(FieldRegistry registry)
{
RootField[] fields = Empty;
if (Fields != null)
{
fields = new Field[Fields.Count];
fields = new RootField[Fields.Count];
for (var i = 0; i < fields.Length; i++)
{
@ -67,7 +86,29 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
var parititonKey = new Partitioning(fieldModel.Partitioning);
var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties);
var field = registry.CreateRootField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties);
if (field is ArrayField arrayField && fieldModel.Children?.Count > 0)
{
foreach (var nestedFieldModel in fieldModel.Children)
{
var nestedField = registry.CreateNestedField(nestedFieldModel.Id, nestedFieldModel.Name, nestedFieldModel.Properties);
if (nestedFieldModel.IsHidden)
{
nestedField = nestedField.Hide();
}
if (nestedFieldModel.IsDisabled)
{
nestedField = nestedField.Disable();
}
arrayField = arrayField.AddField(nestedField);
}
field = arrayField;
}
if (fieldModel.IsDisabled)
{

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

@ -22,9 +22,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<JsonFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<JsonFieldProperties>(id, name, partitioning, this);
return Fields.Json(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.Json(id, name, this);
}
}
}

106
src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs

@ -0,0 +1,106 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class NestedField : Cloneable<NestedField>, INestedField
{
private readonly long fieldId;
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 abstract FieldProperties RawProperties { get; }
protected NestedField(long id, string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.GreaterThan(id, 0, nameof(id));
fieldId = id;
fieldName = name;
}
[Pure]
public NestedField Lock()
{
return Clone(clone =>
{
clone.isLocked = true;
});
}
[Pure]
public NestedField Hide()
{
return Clone(clone =>
{
clone.isHidden = true;
});
}
[Pure]
public NestedField Show()
{
return Clone(clone =>
{
clone.isHidden = false;
});
}
[Pure]
public NestedField Disable()
{
return Clone(clone =>
{
clone.isDisabled = true;
});
}
[Pure]
public NestedField Enable()
{
return Clone(clone =>
{
clone.isDisabled = false;
});
}
public abstract T Accept<T>(IFieldVisitor<T> visitor);
public abstract NestedField Update(FieldProperties newProperties);
}
}

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

@ -0,0 +1,70 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public class NestedField<T> : NestedField, IField<T> where T : FieldProperties, new()
{
private T properties;
public T Properties
{
get { return properties; }
}
public override FieldProperties RawProperties
{
get { return properties; }
}
public NestedField(long id, string name, T properties)
: base(id, name)
{
Guard.NotNull(properties, nameof(properties));
SetProperties(properties);
}
[Pure]
public override NestedField Update(FieldProperties newProperties)
{
var typedProperties = ValidateProperties(newProperties);
return Clone<NestedField<T>>(clone =>
{
clone.SetProperties(typedProperties);
});
}
private void SetProperties(T newProperties)
{
properties = newProperties;
properties.Freeze();
}
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;
}
public override TResult Accept<TResult>(IFieldVisitor<TResult> visitor)
{
return properties.Accept(visitor, this);
}
}
}

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

@ -35,9 +35,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<NumberFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<NumberFieldProperties>(id, name, partitioning, this);
return Fields.Number(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.Number(id, name, this);
}
}
}

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

@ -29,9 +29,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<ReferencesFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<ReferencesFieldProperties>(id, name, partitioning, this);
return Fields.References(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.References(id, name, this);
}
}
}

24
src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs → src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs

@ -10,11 +10,11 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class Field : Cloneable<Field>, IField
public abstract class RootField : Cloneable<RootField>, IRootField
{
private readonly long fieldId;
private readonly Partitioning partitioning;
private readonly string fieldName;
private readonly Partitioning partitioning;
private bool isDisabled;
private bool isHidden;
private bool isLocked;
@ -51,11 +51,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
public abstract FieldProperties RawProperties { get; }
protected Field(long id, string name, Partitioning partitioning)
protected RootField(long id, string name, Partitioning partitioning)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(partitioning, nameof(partitioning));
Guard.GreaterThan(id, 0, nameof(id));
Guard.NotNull(partitioning, nameof(partitioning));
fieldId = id;
fieldName = name;
@ -64,16 +64,16 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Field Lock()
public RootField Lock()
{
return Clone<Field>(clone =>
return Clone(clone =>
{
clone.isLocked = true;
});
}
[Pure]
public Field Hide()
public RootField Hide()
{
return Clone(clone =>
{
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Field Show()
public RootField Show()
{
return Clone(clone =>
{
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Field Disable()
public RootField Disable()
{
return Clone(clone =>
{
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Field Enable()
public RootField Enable()
{
return Clone(clone =>
{
@ -108,8 +108,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
});
}
public abstract Field Update(FieldProperties newProperties);
public abstract T Accept<T>(IFieldVisitor<T> visitor);
public abstract RootField Update(FieldProperties newProperties);
}
}

22
src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs → src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class Field<T> : Field, IField<T> where T : FieldProperties, new()
public class RootField<T> : RootField, IField<T> where T : FieldProperties, new()
{
private T properties;
@ -25,27 +25,31 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return properties; }
}
public Field(long id, string name, Partitioning partitioning, T properties)
public RootField(long id, string name, Partitioning partitioning, T properties)
: base(id, name, partitioning)
{
Guard.NotNull(properties, nameof(properties));
this.properties = properties;
this.properties.Freeze();
SetProperties(properties);
}
[Pure]
public override Field Update(FieldProperties newProperties)
public override RootField Update(FieldProperties newProperties)
{
var typedProperties = ValidateProperties(newProperties);
return Clone<Field<T>>(clone =>
return Clone<RootField<T>>(clone =>
{
clone.properties = typedProperties;
clone.properties.Freeze();
clone.SetProperties(typedProperties);
});
}
private void SetProperties(T newProperties)
{
properties = newProperties;
properties.Freeze();
}
private T ValidateProperties(FieldProperties newProperties)
{
Guard.NotNull(newProperties, nameof(newProperties));
@ -60,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public override TResult Accept<TResult>(IFieldVisitor<TResult> visitor)
{
return RawProperties.Accept(visitor, this);
return properties.Accept(visitor, this);
}
}
}

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

@ -7,9 +7,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
@ -17,9 +15,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public sealed class Schema : Cloneable<Schema>
{
private readonly string name;
private ImmutableArray<Field> fieldsOrdered = ImmutableArray<Field>.Empty;
private ImmutableDictionary<long, Field> fieldsById;
private ImmutableDictionary<string, Field> fieldsByName;
private FieldCollection<RootField> fields = FieldCollection<RootField>.Empty;
private SchemaProperties properties;
private bool isPublished;
@ -33,49 +29,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return isPublished; }
}
public IReadOnlyList<Field> Fields
public IReadOnlyList<RootField> Fields
{
get { return fieldsOrdered; }
get { return fields.Ordered; }
}
public IReadOnlyDictionary<long, Field> FieldsById
public IReadOnlyDictionary<long, RootField> FieldsById
{
get
{
if (fieldsById == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsById = ImmutableDictionary<long, Field>.Empty;
}
else
{
fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
}
}
return fieldsById;
}
get { return fields.ById; }
}
public IReadOnlyDictionary<string, Field> FieldsByName
public IReadOnlyDictionary<string, RootField> FieldsByName
{
get
{
if (fieldsByName == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsByName = ImmutableDictionary<string, Field>.Empty;
}
else
{
fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name);
}
}
return fieldsByName;
}
get { return fields.ByName; }
}
public SchemaProperties Properties
@ -93,21 +59,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.properties.Freeze();
}
public Schema(string name, Field[] fields, SchemaProperties properties, bool isPublished)
public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished)
: this(name, properties)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(fields, nameof(fields));
this.isPublished = isPublished;
fieldsOrdered = ImmutableArray.Create(fields);
}
this.fields = new FieldCollection<RootField>(fields);
protected override void OnCloned()
{
fieldsById = null;
fieldsByName = null;
this.isPublished = isPublished;
}
[Pure]
@ -122,60 +81,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
});
}
[Pure]
public Schema UpdateField(long fieldId, FieldProperties newProperties)
{
return UpdateField(fieldId, field =>
{
return field.Update(newProperties);
});
}
[Pure]
public Schema LockField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Lock();
});
}
[Pure]
public Schema DisableField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Disable();
});
}
[Pure]
public Schema EnableField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Enable();
});
}
[Pure]
public Schema HideField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Hide();
});
}
[Pure]
public Schema ShowField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Show();
});
}
[Pure]
public Schema Publish()
{
@ -197,62 +102,39 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public Schema DeleteField(long fieldId)
{
if (!FieldsById.TryGetValue(fieldId, out var field))
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.Remove(field);
});
return Updatefields(f => f.Remove(fieldId));
}
[Pure]
public Schema ReorderFields(List<long> ids)
{
Guard.NotNull(ids, nameof(ids));
if (ids.Count != fieldsOrdered.Length || ids.Any(x => !FieldsById.ContainsKey(x)))
{
throw new ArgumentException("Ids must cover all fields.", nameof(ids));
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToImmutableArray();
});
return Updatefields(f => f.Reorder(ids));
}
[Pure]
public Schema AddField(Field field)
public Schema AddField(RootField field)
{
Guard.NotNull(field, nameof(field));
if (FieldsByName.ContainsKey(field.Name) || FieldsById.ContainsKey(field.Id))
{
throw new ArgumentException($"A field with name '{field.Name}' and id {field.Id} already exists.", nameof(field));
}
return Clone(clone =>
{
clone.fieldsOrdered = clone.fieldsOrdered.Add(field);
});
return Updatefields(f => f.Add(field));
}
[Pure]
public Schema UpdateField(long fieldId, Func<Field, Field> updater)
public Schema UpdateField(long fieldId, Func<RootField, RootField> updater)
{
return Updatefields(f => f.Update(fieldId, updater));
}
private Schema Updatefields(Func<FieldCollection<RootField>, FieldCollection<RootField>> updater)
{
Guard.NotNull(updater, nameof(updater));
var newFields = updater(fields);
if (!FieldsById.TryGetValue(fieldId, out var field))
if (ReferenceEquals(newFields, fields))
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = clone.fieldsOrdered.Replace(field, updater(field));
clone.fields = newFields;
});
}
}

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

@ -39,9 +39,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<StringFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<StringFieldProperties>(id, name, partitioning, this);
return Fields.String(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.String(id, name, this);
}
}
}

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

@ -26,9 +26,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit((IField<TagsFieldProperties>)field);
}
public override Field CreateField(long id, string name, Partitioning partitioning)
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return new Field<TagsFieldProperties>(id, name, partitioning, this);
return Fields.Tags(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.Tags(id, name, this);
}
}
}

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

@ -5,128 +5,92 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
public delegate ContentFieldData FieldConverter(ContentFieldData data, Field field);
public static class ContentConverter
{
public static NamedContentData ToNameModel(this IdContentData source, Schema schema, params FieldConverter[] converters)
private static readonly Func<IRootField, string> KeyNameResolver = f => f.Name;
private static readonly Func<IRootField, long> KeyIdResolver = f => f.Id;
public static NamedContentData ConvertId2Name(this IdContentData content, Schema schema, params FieldConverter[] converters)
{
Guard.NotNull(schema, nameof(schema));
var result = new NamedContentData();
foreach (var fieldValue in source)
{
if (!schema.FieldsById.TryGetValue(fieldValue.Key, out var field))
{
continue;
}
var fieldData = Convert(fieldValue.Value, field, converters);
if (fieldData != null)
{
result[field.Name] = fieldData;
}
}
var result = new NamedContentData(content.Count);
return result;
return ConvertInternal(content, result, schema.FieldsById, KeyNameResolver, converters);
}
public static IdContentData ToIdModel(this NamedContentData content, Schema schema, params FieldConverter[] converters)
public static IdContentData ConvertId2Id(this IdContentData content, Schema schema, params FieldConverter[] converters)
{
Guard.NotNull(schema, nameof(schema));
var result = new IdContentData();
var result = new IdContentData(content.Count);
foreach (var fieldValue in content)
{
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field))
{
continue;
}
var fieldData = Convert(fieldValue.Value, field, converters);
if (fieldData != null)
{
result[field.Id] = fieldData;
}
}
return result;
return ConvertInternal(content, result, schema.FieldsById, KeyIdResolver, converters);
}
public static IdContentData Convert(this IdContentData content, Schema schema, params FieldConverter[] converters)
public static NamedContentData ConvertName2Name(this NamedContentData content, Schema schema, params FieldConverter[] converters)
{
Guard.NotNull(schema, nameof(schema));
var result = new IdContentData();
foreach (var fieldValue in content)
{
if (!schema.FieldsById.TryGetValue(fieldValue.Key, out var field))
{
continue;
}
var fieldData = Convert(fieldValue.Value, field, converters);
if (fieldData != null)
{
result[field.Id] = fieldData;
}
}
var result = new NamedContentData(content.Count);
return result;
return ConvertInternal(content, result, schema.FieldsByName, KeyNameResolver, converters);
}
public static NamedContentData Convert(this NamedContentData content, Schema schema, params FieldConverter[] converters)
public static IdContentData ConvertName2Id(this NamedContentData content, Schema schema, params FieldConverter[] converters)
{
Guard.NotNull(schema, nameof(schema));
var result = new NamedContentData();
var result = new IdContentData(content.Count);
foreach (var fieldValue in content)
return ConvertInternal(content, result, schema.FieldsByName, KeyIdResolver, converters);
}
private static TDict2 ConvertInternal<TKey1, TKey2, TDict1, TDict2>(
TDict1 source,
TDict2 target,
IReadOnlyDictionary<TKey1, RootField> fields,
Func<IRootField, TKey2> targetKey, params FieldConverter[] converters)
where TDict1 : IDictionary<TKey1, ContentFieldData>
where TDict2 : IDictionary<TKey2, ContentFieldData>
{
foreach (var fieldKvp in source)
{
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field))
if (!fields.TryGetValue(fieldKvp.Key, out var field))
{
continue;
}
var fieldData = Convert(fieldValue.Value, field, converters);
var newvalue = fieldKvp.Value;
if (fieldData != null)
if (converters != null)
{
result[field.Name] = fieldData;
}
}
foreach (var converter in converters)
{
newvalue = converter(newvalue, field);
return result;
}
if (newvalue == null)
{
break;
}
}
}
private static ContentFieldData Convert(ContentFieldData fieldData, Field field, FieldConverter[] converters)
{
if (converters != null)
{
foreach (var converter in converters)
if (newvalue != null)
{
fieldData = converter(fieldData, field);
if (fieldData == null)
{
break;
}
target.Add(targetKey(field), newvalue);
}
}
return fieldData;
return target;
}
}
}

147
src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs

@ -8,7 +8,6 @@
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;
@ -21,8 +20,17 @@ using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
public delegate ContentFieldData FieldConverter(ContentFieldData data, IRootField field);
public static class FieldConverters
{
private static readonly Func<IField, string> KeyNameResolver = f => f.Name;
private static readonly Func<IField, string> KeyIdResolver = f => f.Id.ToString();
private static readonly Func<IArrayField, string, IField> FieldByIdResolver =
(f, k) => long.TryParse(k, out var id) ? f.FieldsById.GetOrDefault(id) : null;
private static readonly Func<IArrayField, string, IField> FieldByNameResolver =
(f, k) => f.FieldsByName.GetOrDefault(k);
public static FieldConverter ExcludeHidden()
{
return (data, field) =>
@ -35,29 +43,23 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{
return (data, field) =>
{
var isValid = true;
foreach (var value in data.Values)
{
if (value.IsNull())
{
continue;
}
try
{
if (!value.IsNull())
{
JsonValueConverter.ConvertValue(field, value);
}
JsonValueConverter.ConvertValue(field, value);
}
catch
{
isValid = false;
break;
return null;
}
}
if (!isValid)
{
return null;
}
return data;
};
}
@ -173,12 +175,10 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
return (data, field) => data;
}
var languageCodes =
new HashSet<string>(
languages.Select(x => x.Iso2Code).Where(x => languagesConfig.Contains(x)),
StringComparer.OrdinalIgnoreCase);
var languageCodes = languages.Select(x => x.Iso2Code).Where(x => languagesConfig.Contains(x));
var languageSet = new HashSet<string>(languageCodes, StringComparer.OrdinalIgnoreCase);
if (languageCodes.Count == 0)
if (languageSet.Count == 0)
{
return (data, field) => data;
}
@ -189,7 +189,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{
var result = new ContentFieldData();
foreach (var languageCode in languageCodes)
foreach (var languageCode in languageSet)
{
if (data.TryGetValue(languageCode, out var value))
{
@ -204,26 +204,87 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
};
}
public static FieldConverter DecodeJson()
public static FieldConverter ForNestedName2Name(params ValueConverter[] converters)
{
return ForNested(FieldByNameResolver, KeyNameResolver, converters);
}
public static FieldConverter ForNestedName2Id(params ValueConverter[] converters)
{
return ForNested(FieldByNameResolver, KeyIdResolver, converters);
}
public static FieldConverter ForNestedId2Name(params ValueConverter[] converters)
{
return ForNested(FieldByIdResolver, KeyNameResolver, converters);
}
public static FieldConverter ForNestedId2Id(params ValueConverter[] converters)
{
return ForNested(FieldByIdResolver, KeyIdResolver, converters);
}
private static FieldConverter ForNested(
Func<IArrayField, string, IField> fieldResolver,
Func<IField, string> keyResolver,
params ValueConverter[] converters)
{
return (data, field) =>
{
if (field is IField<JsonFieldProperties>)
if (field is IArrayField arrayField)
{
var result = new ContentFieldData();
foreach (var partitionValue in data)
foreach (var partition in data)
{
if (partitionValue.Value.IsNull())
if (!(partition.Value is JArray jArray))
{
result[partitionValue.Key] = null;
continue;
}
else
var newArray = new JArray();
foreach (JObject item in jArray.OfType<JObject>())
{
var value = Encoding.UTF8.GetString(Convert.FromBase64String(partitionValue.Value.ToString()));
var newItem = new JObject();
result[partitionValue.Key] = JToken.Parse(value);
foreach (var kvp in item)
{
var nestedField = fieldResolver(arrayField, kvp.Key);
if (nestedField == null)
{
continue;
}
var newValue = kvp.Value;
var isUnset = false;
if (converters != null)
{
foreach (var converter in converters)
{
newValue = converter(newValue, nestedField);
if (ReferenceEquals(newValue, Value.Unset))
{
isUnset = true;
break;
}
}
}
if (!isUnset)
{
newItem.Add(keyResolver(nestedField), newValue);
}
}
newArray.Add(newItem);
}
result.Add(partition.Key, newArray);
}
return result;
@ -233,25 +294,37 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
};
}
public static FieldConverter EncodeJson()
public static FieldConverter ForValues(params ValueConverter[] converters)
{
return (data, field) =>
{
if (field is IField<JsonFieldProperties>)
if (!(field is IArrayField))
{
var result = new ContentFieldData();
foreach (var partitionValue in data)
foreach (var partition in data)
{
if (partitionValue.Value.IsNull())
var newValue = partition.Value;
var isUnset = false;
if (converters != null)
{
result[partitionValue.Key] = null;
foreach (var converter in converters)
{
newValue = converter(newValue, field);
if (ReferenceEquals(newValue, Value.Unset))
{
isUnset = true;
break;
}
}
}
else
{
var value = Convert.ToBase64String(Encoding.UTF8.GetBytes(partitionValue.Value.ToString()));
result[partitionValue.Key] = value;
if (!isUnset)
{
result.Add(partition.Key, newValue);
}
}

16
src/Squidex.Domain.Apps.Core.Operations/ConvertContent/Value.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
public static class Value
{
public static readonly JToken Unset = JValue.CreateUndefined();
}
}

81
src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ValueConverters.cs

@ -0,0 +1,81 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Text;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ConvertContent
{
public delegate JToken ValueConverter(JToken value, IField field);
public static class ValueConverters
{
public static ValueConverter DecodeJson()
{
return (value, field) =>
{
if (!value.IsNull() && field is IField<JsonFieldProperties>)
{
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(value.ToString()));
return JToken.Parse(decoded);
}
return value;
};
}
public static ValueConverter EncodeJson()
{
return (value, field) =>
{
if (!value.IsNull() && field is IField<JsonFieldProperties>)
{
var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(value.ToString()));
return encoded;
}
return value;
};
}
public static ValueConverter ExcludeHidden()
{
return (value, field) =>
{
return field.IsHidden ? Value.Unset : value;
};
}
public static ValueConverter ExcludeChangedTypes()
{
return (value, field) =>
{
if (value.IsNull())
{
return value;
}
try
{
JsonValueConverter.ConvertValue(field, value);
}
catch
{
return Value.Unset;
}
return value;
};
}
}
}

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

@ -13,7 +13,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.EnrichContent
{
public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JToken>
public sealed class DefaultValueFactory : IFieldVisitor<JToken>
{
private readonly Instant now;
@ -26,62 +26,67 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
{
Guard.NotNull(field, nameof(field));
return field.RawProperties.Accept(new DefaultValueFactory(now));
return field.Accept(new DefaultValueFactory(now));
}
public JToken Visit(AssetsFieldProperties properties)
public JToken Visit(IArrayField field)
{
return new JArray();
}
public JToken Visit(BooleanFieldProperties properties)
public JToken Visit(IField<AssetsFieldProperties> field)
{
return properties.DefaultValue;
return new JArray();
}
public JToken Visit(IField<BooleanFieldProperties> field)
{
return field.Properties.DefaultValue;
}
public JToken Visit(GeolocationFieldProperties properties)
public JToken Visit(IField<GeolocationFieldProperties> field)
{
return JValue.CreateNull();
}
public JToken Visit(JsonFieldProperties properties)
public JToken Visit(IField<JsonFieldProperties> field)
{
return JValue.CreateNull();
}
public JToken Visit(NumberFieldProperties properties)
public JToken Visit(IField<NumberFieldProperties> field)
{
return properties.DefaultValue;
return field.Properties.DefaultValue;
}
public JToken Visit(ReferencesFieldProperties properties)
public JToken Visit(IField<ReferencesFieldProperties> field)
{
return new JArray();
}
public JToken Visit(StringFieldProperties properties)
public JToken Visit(IField<StringFieldProperties> field)
{
return properties.DefaultValue;
return field.Properties.DefaultValue;
}
public JToken Visit(TagsFieldProperties properties)
public JToken Visit(IField<TagsFieldProperties> field)
{
return new JArray();
}
public JToken Visit(DateTimeFieldProperties properties)
public JToken Visit(IField<DateTimeFieldProperties> field)
{
if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now)
if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now)
{
return now.ToString();
}
if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today)
if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today)
{
return now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
return properties.DefaultValue?.ToString();
return field.Properties.DefaultValue?.ToString();
}
}
}

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

@ -7,65 +7,95 @@
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 sealed class ReferencesCleaner : IFieldVisitor<JToken>
{
private static readonly List<Guid> EmptyIds = new List<Guid>();
private readonly JToken value;
private readonly ICollection<Guid> oldReferences;
public static JToken CleanReferences(this IField field, JToken value, ISet<Guid> oldReferences)
private ReferencesCleaner(JToken value, ICollection<Guid> oldReferences)
{
if ((field is IField<AssetsFieldProperties> || field is IField<ReferencesFieldProperties>) && !value.IsNull())
{
switch (field)
{
case IField<AssetsFieldProperties> assetsField:
return Visit(assetsField, value, oldReferences);
case IField<ReferencesFieldProperties> referencesField:
return Visit(referencesField, value, oldReferences);
}
}
this.value = value;
return value;
this.oldReferences = oldReferences;
}
private static JToken Visit(IField<AssetsFieldProperties> field, JToken value, IEnumerable<Guid> oldReferences)
public static JToken CleanReferences(IField field, JToken value, ICollection<Guid> oldReferences)
{
var oldIds = GetIds(value);
var newIds = oldIds.Except(oldReferences).ToList();
return field.Accept(new ReferencesCleaner(value, oldReferences));
}
return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value;
public JToken Visit(IArrayField field)
{
return value;
}
private static JToken Visit(IField<ReferencesFieldProperties> field, JToken value, ICollection<Guid> oldReferences)
public JToken Visit(IField<AssetsFieldProperties> field)
{
return CleanIds();
}
public JToken Visit(IField<ReferencesFieldProperties> field)
{
if (oldReferences.Contains(field.Properties.SchemaId))
{
return new JArray();
}
var oldIds = GetIds(value);
var newIds = oldIds.Except(oldReferences).ToList();
return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value;
return CleanIds();
}
private static List<Guid> GetIds(JToken value)
private JToken CleanIds()
{
try
{
return value?.ToObject<List<Guid>>() ?? EmptyIds;
}
catch
var ids = value.ToGuidSet();
var isRemoved = false;
foreach (var oldReference in oldReferences)
{
return EmptyIds;
isRemoved |= ids.Remove(oldReference);
}
return isRemoved ? ids.ToJToken() : value;
}
public JToken Visit(IField<BooleanFieldProperties> field)
{
return value;
}
public JToken Visit(IField<DateTimeFieldProperties> field)
{
return value;
}
public JToken Visit(IField<GeolocationFieldProperties> field)
{
return value;
}
public JToken Visit(IField<JsonFieldProperties> field)
{
return value;
}
public JToken Visit(IField<NumberFieldProperties> field)
{
return value;
}
public JToken Visit(IField<StringFieldProperties> field)
{
return value;
}
public JToken Visit(IField<TagsFieldProperties> field)
{
return value;
}
}
}

69
src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtensions.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ReferencesExtensions
{
public static IEnumerable<Guid> ExtractReferences(this IField field, JToken value)
{
return ReferencesExtractor.ExtractReferences(field, value);
}
public static JToken CleanReferences(this IField field, JToken value, ICollection<Guid> oldReferences)
{
if (value.IsNull())
{
return value;
}
return ReferencesCleaner.CleanReferences(field, value, oldReferences);
}
public static JToken ToJToken(this HashSet<Guid> ids)
{
var result = new JArray();
foreach (var id in ids)
{
result.Add(new JValue(id));
}
return result;
}
public static HashSet<Guid> ToGuidSet(this JToken value)
{
if (value is JArray ids)
{
var result = new HashSet<Guid>();
foreach (var id in ids)
{
if (id.Type == JTokenType.Guid)
{
result.Add((Guid)id);
}
else if (id.Type == JTokenType.String && Guid.TryParse((string)id, out var guid))
{
result.Add(guid);
}
}
return result;
}
return new HashSet<Guid>();
}
}
}

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

@ -13,50 +13,93 @@ using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class ReferencesExtractor
public sealed class ReferencesExtractor : IFieldVisitor<IEnumerable<Guid>>
{
public static IEnumerable<Guid> ExtractReferences(this IField field, JToken value)
{
switch (field)
{
case IField<AssetsFieldProperties> assetsField:
return Visit(assetsField, value);
private readonly JToken value;
case IField<ReferencesFieldProperties> referencesField:
return Visit(referencesField, value);
}
private ReferencesExtractor(JToken value)
{
this.value = value;
}
return Enumerable.Empty<Guid>();
public static IEnumerable<Guid> ExtractReferences(IField field, JToken value)
{
return field.Accept(new ReferencesExtractor(value));
}
public static IEnumerable<Guid> Visit(IField<AssetsFieldProperties> field, JToken value)
public IEnumerable<Guid> Visit(IArrayField field)
{
IEnumerable<Guid> result;
try
{
result = value?.ToObject<List<Guid>>();
}
catch
var result = new List<Guid>();
if (value is JArray items)
{
result = null;
foreach (JObject item in value)
{
foreach (var nestedField in field.Fields)
{
if (item.TryGetValue(field.Name, out var value))
{
result.AddRange(nestedField.Accept(new ReferencesExtractor(value)));
}
}
}
}
return result ?? Enumerable.Empty<Guid>();
return result;
}
private static IEnumerable<Guid> Visit(IField<ReferencesFieldProperties> field, JToken value)
public IEnumerable<Guid> Visit(IField<AssetsFieldProperties> field)
{
IEnumerable<Guid> result;
try
{
result = value?.ToObject<List<Guid>>() ?? Enumerable.Empty<Guid>();
}
catch
var ids = value.ToGuidSet();
return ids;
}
public IEnumerable<Guid> Visit(IField<ReferencesFieldProperties> field)
{
var ids = value.ToGuidSet();
if (field.Properties.SchemaId != Guid.Empty)
{
result = Enumerable.Empty<Guid>();
ids.Add(field.Properties.SchemaId);
}
return result.Union(new[] { field.Properties.SchemaId });
return ids;
}
public IEnumerable<Guid> Visit(IField<BooleanFieldProperties> field)
{
return Enumerable.Empty<Guid>();
}
public IEnumerable<Guid> Visit(IField<DateTimeFieldProperties> field)
{
return Enumerable.Empty<Guid>();
}
public IEnumerable<Guid> Visit(IField<GeolocationFieldProperties> field)
{
return Enumerable.Empty<Guid>();
}
public IEnumerable<Guid> Visit(IField<JsonFieldProperties> field)
{
return Enumerable.Empty<Guid>();
}
public IEnumerable<Guid> Visit(IField<NumberFieldProperties> field)
{
return Enumerable.Empty<Guid>();
}
public IEnumerable<Guid> Visit(IField<StringFieldProperties> field)
{
return Enumerable.Empty<Guid>();
}
public IEnumerable<Guid> Visit(IField<TagsFieldProperties> field)
{
return Enumerable.Empty<Guid>();
}
}
}

15
src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/FieldReferencesConverter.cs → src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ValueReferencesConverter.cs

@ -7,28 +7,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds
{
public static class FieldReferencesConverter
public static class ValueReferencesConverter
{
public static FieldConverter CleanReferences(IEnumerable<Guid> deletedReferencedIds)
public static ValueConverter CleanReferences(IEnumerable<Guid> deletedReferencedIds)
{
var ids = new HashSet<Guid>(deletedReferencedIds);
return (data, field) =>
return (value, field) =>
{
foreach (var partitionValue in data.Where(x => !x.Value.IsNull()).ToList())
if (value.IsNull())
{
var newValue = field.CleanReferences(partitionValue.Value, ids);
data[partitionValue.Key] = newValue;
return value;
}
return data;
return field.CleanReferences(value, ids);
};
}
}

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

@ -23,6 +23,11 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema
return field.Accept(Instance);
}
public IEdmTypeReference Visit(IArrayField field)
{
return null;
}
public IEdmTypeReference Visit(IField<AssetsFieldProperties> field)
{
return CreatePrimitive(EdmPrimitiveTypeKind.String, field);

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

@ -7,6 +7,7 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using NJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
@ -21,6 +22,30 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
this.schemaResolver = schemaResolver;
}
public JsonProperty Visit(IArrayField field)
{
return CreateProperty(field, jsonProperty =>
{
var itemSchema = new JsonSchema4
{
Type = JsonObjectType.Object
};
foreach (var nestedField in field.Fields.Where(x => !x.IsHidden))
{
var childProperty = nestedField.Accept(this);
childProperty.Description = nestedField.RawProperties.Hints;
childProperty.IsRequired = nestedField.RawProperties.IsRequired;
itemSchema.Properties.Add(nestedField.Name, childProperty);
}
jsonProperty.Type = JsonObjectType.Object;
jsonProperty.Item = itemSchema;
});
}
public JsonProperty Visit(IField<AssetsFieldProperties> field)
{
return CreateProperty(field, jsonProperty =>

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

@ -95,14 +95,14 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
EnsurePropertiesInitialized();
fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this)).Value = value;
fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c)).Value = value;
}
public override PropertyDescriptor GetOwnProperty(string propertyName)
{
EnsurePropertiesInitialized();
return fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this, new ContentFieldObject(this, new ContentFieldData(), false)));
return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false)));
}
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties()

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

@ -7,18 +7,22 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure;
#pragma warning disable 168
#pragma warning disable SA1028, IDE0004 // Code must not contain trailing whitespace
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class ContentValidator
{
private static readonly ContentFieldData DefaultFieldData = new ContentFieldData();
private static readonly JToken DefaultValue = JValue.CreateNull();
private readonly Schema schema;
private readonly PartitionResolver partitionResolver;
private readonly ValidationContext context;
@ -39,103 +43,60 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
this.partitionResolver = partitionResolver;
}
public Task ValidatePartialAsync(NamedContentData data)
private void AddError(IEnumerable<string> path, string message)
{
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));
}
}
var pathString = path.ToPathString();
return Task.WhenAll(tasks);
errors.Add(new ValidationError($"{pathString}: {message}", pathString));
}
private Task ValidateFieldPartialAsync(Field field, ContentFieldData fieldData)
public Task ValidatePartialAsync(NamedContentData data)
{
var partitioning = field.Partitioning;
var partition = partitionResolver(partitioning);
var tasks = new List<Task>();
Guard.NotNull(data, nameof(data));
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);
}
}
var validator = CreateSchemaValidator(true);
return Task.WhenAll(tasks);
return validator.ValidateAsync(data, context, AddError);
}
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));
}
var validator = CreateSchemaValidator(false);
return Task.WhenAll(tasks);
return validator.ValidateAsync(data, context, AddError);
}
private void ValidateUnknownFields(NamedContentData data)
private IValidator CreateSchemaValidator(bool isPartial)
{
foreach (var fieldData in data)
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
foreach (var field in schema.FieldsByName)
{
if (!schema.FieldsByName.ContainsKey(fieldData.Key))
{
errors.AddError("<FIELD> is not a known field.", fieldData.Key);
}
fieldsValidators[field.Key] = (!field.Value.RawProperties.IsRequired, CreateFieldValidator(field.Value, isPartial));
}
return new ObjectValidator<ContentFieldData>(fieldsValidators, isPartial, "field", DefaultFieldData);
}
private Task ValidateFieldAsync(Field field, ContentFieldData fieldData)
private IValidator CreateFieldValidator(IRootField field, bool isPartial)
{
var partitioning = field.Partitioning;
var partition = partitionResolver(partitioning);
var partitioning = partitionResolver(field.Partitioning);
var tasks = new List<Task>();
var fieldValidator = new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field);
var fieldsValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
foreach (var partitionValues in fieldData)
foreach (var partition in partitioning)
{
if (!partition.TryGetItem(partitionValues.Key, out var _))
{
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field);
}
fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator);
}
foreach (var item in partition)
{
var value = fieldData.GetOrCreate(item.Key, k => JValue.CreateNull());
var isLanguage = field.Partitioning.Equals(Partitioning.Language);
tasks.Add(field.ValidateAsync(value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item)));
}
var type = isLanguage ? "language" : "invariant value";
return Task.WhenAll(tasks);
return new ObjectValidator<JToken>(fieldsValidators, isPartial, type, DefaultValue);
}
}
}

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

@ -1,57 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
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, IField 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 IField 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.");
}
}
}
}

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

@ -16,11 +16,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class JsonValueConverter : IFieldVisitor<object>
{
public JToken Value { get; }
private readonly JToken value;
private JsonValueConverter(JToken value)
{
Value = value;
this.value = value;
}
public static object ConvertValue(IField field, JToken json)
@ -28,21 +28,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return field.Accept(new JsonValueConverter(json));
}
public object Visit(IArrayField field)
{
return value.ToObject<List<JObject>>();
}
public object Visit(IField<AssetsFieldProperties> field)
{
return Value.ToObject<List<Guid>>();
return value.ToObject<List<Guid>>();
}
public object Visit(IField<BooleanFieldProperties> field)
{
return (bool?)Value;
return (bool?)value;
}
public object Visit(IField<DateTimeFieldProperties> field)
{
if (Value.Type == JTokenType.String)
if (value.Type == JTokenType.String)
{
var parseResult = InstantPattern.General.Parse(Value.ToString());
var parseResult = InstantPattern.General.Parse(value.ToString());
if (!parseResult.Success)
{
@ -57,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public object Visit(IField<GeolocationFieldProperties> field)
{
var geolocation = (JObject)Value;
var geolocation = (JObject)value;
foreach (var property in geolocation.Properties())
{
@ -81,32 +86,32 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
throw new InvalidCastException("Longitude must be between -180 and 180.");
}
return Value;
return value;
}
public object Visit(IField<JsonFieldProperties> field)
{
return Value;
return value;
}
public object Visit(IField<NumberFieldProperties> field)
{
return (double?)Value;
return (double?)value;
}
public object Visit(IField<ReferencesFieldProperties> field)
{
return Value.ToObject<List<Guid>>();
return value.ToObject<List<Guid>>();
}
public object Visit(IField<StringFieldProperties> field)
{
return Value.ToString();
return value.ToString();
}
public object Visit(IField<TagsFieldProperties> field)
{
return Value.ToObject<List<string>>();
return value.ToObject<List<string>>();
}
}
}

52
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ObjectPath.cs

@ -0,0 +1,52 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Text;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public static class ObjectPath
{
public static string ToPathString(this IEnumerable<string> path)
{
var sb = new StringBuilder();
var index = 0;
foreach (var property in path)
{
if (index == 0)
{
sb.Append(property);
}
else if (index == 1)
{
if (!property.Equals(InvariantPartitioning.Instance.Master.Key, StringComparison.OrdinalIgnoreCase))
{
sb.Append("(");
sb.Append(property);
sb.Append(")");
}
}
else
{
if (property[0] != '[')
{
sb.Append(".");
}
sb.Append(property);
}
index++;
}
return sb.ToString();
}
}
}

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

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@ -16,24 +17,33 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
private readonly Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent;
private readonly Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset;
private readonly ImmutableQueue<string> propertyPath;
public ImmutableQueue<string> Path
{
get { return propertyPath; }
}
public bool IsOptional { get; }
public ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent,
Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset)
: this(checkContent, checkAsset, false)
: this(checkContent, checkAsset, ImmutableQueue<string>.Empty, false)
{
}
private ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent,
Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset,
ImmutableQueue<string> propertyPath,
bool isOptional)
{
Guard.NotNull(checkAsset, nameof(checkAsset));
Guard.NotNull(checkContent, nameof(checkAsset));
this.propertyPath = propertyPath;
this.checkContent = checkContent;
this.checkAsset = checkAsset;
@ -42,7 +52,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public ValidationContext Optional(bool isOptional)
{
return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, isOptional);
return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, propertyPath, isOptional);
}
public ValidationContext Nested(string property)
{
return new ValidationContext(checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional);
}
public Task<IReadOnlyList<Guid>> GetInvalidContentIdsAsync(IEnumerable<Guid> contentIds, Guid schemaId)

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

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
@ -24,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.allowedValues = allowedValues;
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value == null)
{
@ -35,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (!allowedValues.Contains(typedValue))
{
addError("<FIELD> is not an allowed value.");
addError(context.Path, "Not an allowed value.");
}
return TaskHelper.Done;

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

@ -23,52 +23,49 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.properties = properties;
}
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is ICollection<Guid> assetIds)
if (value is ICollection<Guid> assetIds && assetIds.Count > 0)
{
var assets = await context.GetAssetInfosAsync(assetIds);
var i = 0;
var index = 0;
foreach (var assetId in assetIds)
{
i++;
index++;
var asset = assets.FirstOrDefault(x => x.AssetId == assetId);
var path = context.Path.Enqueue($"[{index}]");
void Error(string message)
{
addError($"<FIELD> has invalid asset #{i}: {message}");
}
var asset = assets.FirstOrDefault(x => x.AssetId == assetId);
if (asset == null)
{
Error($"Id '{assetId}' not found.");
addError(path, $"Id '{assetId}' not found.");
continue;
}
if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize)
{
Error($"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'.");
addError(path, $"'{asset.FileSize.ToReadableSize()}' less than minimum of '{properties.MinSize.Value.ToReadableSize()}'.");
}
if (properties.MaxSize.HasValue && asset.FileSize > properties.MaxSize)
{
Error($"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'.");
addError(path, $"'{asset.FileSize.ToReadableSize()}' greater than maximum of '{properties.MaxSize.Value.ToReadableSize()}'.");
}
if (properties.AllowedExtensions != null &&
properties.AllowedExtensions.Count > 0 &&
!properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase)))
{
Error("Invalid file extension.");
addError(path, "Invalid file extension.");
}
if (!asset.IsImage)
{
if (properties.MustBeImage)
{
Error("Not an image.");
addError(path, "Not an image.");
}
continue;
@ -84,22 +81,22 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (properties.MinWidth.HasValue && w < properties.MinWidth)
{
Error($"Width '{w}px' less than minimum of '{properties.MinWidth}px'.");
addError(path, $"Width '{w}px' less than minimum of '{properties.MinWidth}px'.");
}
if (properties.MaxWidth.HasValue && w > properties.MaxWidth)
{
Error($"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'.");
addError(path, $"Width '{w}px' greater than maximum of '{properties.MaxWidth}px'.");
}
if (properties.MinHeight.HasValue && h < properties.MinHeight)
{
Error($"Height '{h}px' less than minimum of '{properties.MinHeight}px'.");
addError(path, $"Height '{h}px' less than minimum of '{properties.MinHeight}px'.");
}
if (properties.MaxHeight.HasValue && h > properties.MaxHeight)
{
Error($"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'.");
addError(path, $"Height '{h}px' greater than maximum of '{properties.MaxHeight}px'.");
}
if (properties.AspectHeight.HasValue && properties.AspectWidth.HasValue)
@ -108,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon)
{
Error($"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'.");
addError(path, $"Aspect ratio not '{properties.AspectWidth}:{properties.AspectHeight}'.");
}
}
}

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

@ -5,14 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class CollectionItemValidator<T> : IValidator
public sealed class CollectionItemValidator : IValidator
{
private readonly IValidator[] itemValidators;
@ -24,23 +24,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.itemValidators = itemValidators;
}
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is ICollection<T> items)
if (value is ICollection items && items.Count > 0)
{
var innerContext = context.Optional(false);
var innerTasks = new List<Task>();
var index = 1;
foreach (var item in items)
{
var innerContext = context.Nested($"[{index}]");
foreach (var itemValidator in itemValidators)
{
await itemValidator.ValidateAsync(item, innerContext, e => addError(e.Replace("<FIELD>", $"<FIELD> item #{index}")));
innerTasks.Add(itemValidator.ValidateAsync(item, innerContext, addError));
}
index++;
}
await Task.WhenAll(innerTasks);
}
}
}

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

@ -5,14 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class CollectionValidator<T> : IValidator
public sealed class CollectionValidator : IValidator
{
private readonly bool isRequired;
private readonly int? minItems;
@ -25,13 +24,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.maxItems = maxItems;
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (!(value is ICollection<T> items) || items.Count == 0)
if (!(value is ICollection items) || items.Count == 0)
{
if (isRequired && !context.IsOptional)
{
addError("<FIELD> is required.");
addError(context.Path, "Field is required.");
}
return TaskHelper.Done;
@ -39,12 +38,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (minItems.HasValue && items.Count < minItems.Value)
{
addError($"<FIELD> must have at least {minItems} item(s).");
addError(context.Path, $"Must have at least {minItems} item(s).");
}
if (maxItems.HasValue && items.Count > maxItems.Value)
{
addError($"<FIELD> must have not more than {maxItems} item(s).");
addError(context.Path, $"Must have not more than {maxItems} item(s).");
}
return TaskHelper.Done;

53
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class FieldValidator : IValidator
{
private readonly IValidator[] validators;
private readonly IField field;
public FieldValidator(IValidator[] validators, IField field)
{
this.validators = validators;
this.field = field;
}
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
try
{
object typedValue = null;
if (value is JToken jToken)
{
typedValue = jToken.IsNull() ? null : JsonValueConverter.ConvertValue(field, jToken);
}
var tasks = new List<Task>();
foreach (var validator in ValidatorsFactory.CreateValidators(field))
{
tasks.Add(validator.ValidateAsync(typedValue, context, addError));
}
await Task.WhenAll(tasks);
}
catch
{
addError(context.Path, "Not a valid value.");
}
}
}
}

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

@ -5,13 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public delegate void AddError(IEnumerable<string> path, string message);
public interface IValidator
{
Task ValidateAsync(object value, ValidationContext context, Action<string> addError);
Task ValidateAsync(object value, ValidationContext context, AddError addError);
}
}

68
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ObjectValidator.cs

@ -0,0 +1,68 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class ObjectValidator<TValue> : IValidator
{
private readonly IDictionary<string, (bool IsOptional, IValidator Validator)> schema;
private readonly bool isPartial;
private readonly string fieldType;
private readonly TValue fieldDefault;
public ObjectValidator(IDictionary<string, (bool IsOptional, IValidator Validator)> schema, bool isPartial, string fieldType, TValue fieldDefault)
{
this.schema = schema;
this.fieldDefault = fieldDefault;
this.fieldType = fieldType;
this.isPartial = isPartial;
}
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is IDictionary<string, TValue> values)
{
foreach (var fieldData in values)
{
var name = fieldData.Key;
if (!schema.ContainsKey(name))
{
addError(context.Path.Enqueue(name), $"Not a known {fieldType}.");
}
}
var tasks = new List<Task>();
foreach (var field in schema)
{
var name = field.Key;
if (!values.TryGetValue(name, out var fieldValue))
{
if (isPartial)
{
continue;
}
fieldValue = fieldDefault;
}
var (isOptional, validator) = field.Value;
var fieldContext = context.Nested(name).Optional(isOptional);
tasks.Add(validator.ValidateAsync(fieldValue, fieldContext, addError));
}
await Task.WhenAll(tasks);
}
}
}
}

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

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
regex = new Regex("^" + pattern + "$", RegexOptions.None, Timeout);
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is string stringValue)
{
@ -37,17 +37,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
if (string.IsNullOrWhiteSpace(errorMessage))
{
addError("<FIELD> is not valid.");
addError(context.Path, "Not valid.");
}
else
{
addError(errorMessage);
addError(context.Path, errorMessage);
}
}
}
catch
{
addError("<FIELD> has a regex that is too slow.");
addError(context.Path, "Regex is too slow.");
}
}
}

6
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.max = max;
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value == null)
{
@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (min.HasValue && typedValue.CompareTo(min.Value) < 0)
{
addError($"<FIELD> must be greater or equals than '{min}'.");
addError(context.Path, $"Must be greater or equals than '{min}'.");
}
if (max.HasValue && typedValue.CompareTo(max.Value) > 0)
{
addError($"<FIELD> must be less or equals than '{max}'.");
addError(context.Path, $"Must be less or equals than '{max}'.");
}
return TaskHelper.Done;

4
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.schemaId = schemaId;
}
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public async Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is ICollection<Guid> contentIds)
{
@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
foreach (var invalidId in invalidIds)
{
addError($"<FIELD> contains invalid reference '{invalidId}'.");
addError(context.Path, $"Contains invalid reference '{invalidId}'.");
}
}
}

5
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
@ -20,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.validateEmptyStrings = validateEmptyStrings;
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (context.IsOptional || (value != null && !(value is string)))
{
@ -31,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (valueAsString == null || (validateEmptyStrings && string.IsNullOrWhiteSpace(valueAsString)))
{
addError("<FIELD> is required.");
addError(context.Path, "Field is required.");
}
return TaskHelper.Done;

5
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
@ -13,11 +12,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public class RequiredValidator : IValidator
{
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value == null && !context.IsOptional)
{
addError("<FIELD> is required.");
addError(context.Path, "Field is required.");
}
return TaskHelper.Done;

6
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs

@ -27,18 +27,18 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.maxLength = maxLength;
}
public Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
public Task ValidateAsync(object value, ValidationContext context, AddError addError)
{
if (value is string stringValue && !string.IsNullOrEmpty(stringValue))
{
if (minLength.HasValue && stringValue.Length < minLength.Value)
{
addError($"<FIELD> must have more than '{minLength}' characters.");
addError(context.Path, $"Must have more than '{minLength}' characters.");
}
if (maxLength.HasValue && stringValue.Length > maxLength.Value)
{
addError($"<FIELD> must have less than '{maxLength}' characters.");
addError(context.Path, $"Must have less than '{maxLength}' characters.");
}
}

96
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
@ -15,7 +16,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent
{
public sealed class ValidatorsFactory : IFieldPropertiesVisitor<IEnumerable<IValidator>>
public sealed class ValidatorsFactory : IFieldVisitor<IEnumerable<IValidator>>
{
private static readonly ValidatorsFactory Instance = new ValidatorsFactory();
@ -27,118 +28,135 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
Guard.NotNull(field, nameof(field));
return field.RawProperties.Accept(Instance);
return field.Accept(Instance);
}
public IEnumerable<IValidator> Visit(AssetsFieldProperties properties)
public IEnumerable<IValidator> Visit(IArrayField field)
{
if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue)
if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue)
{
yield return new CollectionValidator<Guid>(properties.IsRequired, properties.MinItems, properties.MaxItems);
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
yield return new AssetsValidator(properties);
var nestedSchema = new Dictionary<string, (bool IsOptional, IValidator Validator)>();
foreach (var nestedField in field.Fields)
{
nestedSchema[nestedField.Name] = (false, new FieldValidator(nestedField.Accept(this).ToArray(), nestedField));
}
yield return new CollectionItemValidator(new ObjectValidator<JToken>(nestedSchema, false, "field", JValue.CreateNull()));
}
public IEnumerable<IValidator> Visit(IField<AssetsFieldProperties> field)
{
if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue)
{
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
yield return new AssetsValidator(field.Properties);
}
public IEnumerable<IValidator> Visit(BooleanFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<BooleanFieldProperties> field)
{
if (properties.IsRequired)
if (field.Properties.IsRequired)
{
yield return new RequiredValidator();
}
}
public IEnumerable<IValidator> Visit(DateTimeFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<DateTimeFieldProperties> field)
{
if (properties.IsRequired)
if (field.Properties.IsRequired)
{
yield return new RequiredValidator();
}
if (properties.MinValue.HasValue || properties.MaxValue.HasValue)
if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue)
{
yield return new RangeValidator<Instant>(properties.MinValue, properties.MaxValue);
yield return new RangeValidator<Instant>(field.Properties.MinValue, field.Properties.MaxValue);
}
}
public IEnumerable<IValidator> Visit(GeolocationFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<GeolocationFieldProperties> field)
{
if (properties.IsRequired)
if (field.Properties.IsRequired)
{
yield return new RequiredValidator();
}
}
public IEnumerable<IValidator> Visit(JsonFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<JsonFieldProperties> field)
{
if (properties.IsRequired)
if (field.Properties.IsRequired)
{
yield return new RequiredValidator();
}
}
public IEnumerable<IValidator> Visit(NumberFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<NumberFieldProperties> field)
{
if (properties.IsRequired)
if (field.Properties.IsRequired)
{
yield return new RequiredValidator();
}
if (properties.MinValue.HasValue || properties.MaxValue.HasValue)
if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue)
{
yield return new RangeValidator<double>(properties.MinValue, properties.MaxValue);
yield return new RangeValidator<double>(field.Properties.MinValue, field.Properties.MaxValue);
}
if (properties.AllowedValues != null)
if (field.Properties.AllowedValues != null)
{
yield return new AllowedValuesValidator<double>(properties.AllowedValues.ToArray());
yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues.ToArray());
}
}
public IEnumerable<IValidator> Visit(ReferencesFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<ReferencesFieldProperties> field)
{
if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue)
if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue)
{
yield return new CollectionValidator<Guid>(properties.IsRequired, properties.MinItems, properties.MaxItems);
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
if (properties.SchemaId != Guid.Empty)
if (field.Properties.SchemaId != Guid.Empty)
{
yield return new ReferencesValidator(properties.SchemaId);
yield return new ReferencesValidator(field.Properties.SchemaId);
}
}
public IEnumerable<IValidator> Visit(StringFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field)
{
if (properties.IsRequired)
if (field.Properties.IsRequired)
{
yield return new RequiredStringValidator();
}
if (properties.MinLength.HasValue || properties.MaxLength.HasValue)
if (field.Properties.MinLength.HasValue || field.Properties.MaxLength.HasValue)
{
yield return new StringLengthValidator(properties.MinLength, properties.MaxLength);
yield return new StringLengthValidator(field.Properties.MinLength, field.Properties.MaxLength);
}
if (!string.IsNullOrWhiteSpace(properties.Pattern))
if (!string.IsNullOrWhiteSpace(field.Properties.Pattern))
{
yield return new PatternValidator(properties.Pattern, properties.PatternMessage);
yield return new PatternValidator(field.Properties.Pattern, field.Properties.PatternMessage);
}
if (properties.AllowedValues != null)
if (field.Properties.AllowedValues != null)
{
yield return new AllowedValuesValidator<string>(properties.AllowedValues.ToArray());
yield return new AllowedValuesValidator<string>(field.Properties.AllowedValues.ToArray());
}
}
public IEnumerable<IValidator> Visit(TagsFieldProperties properties)
public IEnumerable<IValidator> Visit(IField<TagsFieldProperties> field)
{
if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue)
if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue)
{
yield return new CollectionValidator<string>(properties.IsRequired, properties.MinItems, properties.MaxItems);
yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems);
}
yield return new CollectionItemValidator<string>(new RequiredStringValidator());
yield return new CollectionItemValidator(new RequiredStringValidator());
}
}
}

14
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs

@ -28,12 +28,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public static NamedContentData FromMongoModel(this IdContentData result, Schema schema, List<Guid> deletedIds)
{
return result.ToNameModel(schema, FieldConverters.DecodeJson(), FieldReferencesConverter.CleanReferences(deletedIds));
return result.ConvertId2Name(schema,
FieldConverters.ForValues(
ValueConverters.DecodeJson(),
ValueReferencesConverter.CleanReferences(deletedIds)),
FieldConverters.ForNestedId2Name(
ValueConverters.DecodeJson(),
ValueReferencesConverter.CleanReferences(deletedIds)));
}
public static IdContentData ToMongoModel(this NamedContentData result, Schema schema)
{
return result.ToIdModel(schema, FieldConverters.EncodeJson());
return result.ConvertName2Id(schema,
FieldConverters.ForValues(
ValueConverters.EncodeJson()),
FieldConverters.ForNestedName2Id(
ValueConverters.EncodeJson()));
}
public static string ToFullText<T>(this ContentData<T> data)

8
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -39,11 +39,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id);
var idData = value.Data.ToMongoModel(schema.SchemaDef);
var idDraftData = idData;
if (!ReferenceEquals(value.Data, value.DataDraft))
{
idDraftData = value.DataDraft?.ToMongoModel(schema.SchemaDef);
}
var content = SimpleMapper.Map(value, new MongoContentEntity
{
DataByIds = idData,
DataDraftByIds = value.DataDraft?.ToMongoModel(schema.SchemaDef),
DataDraftByIds = idDraftData,
IsDeleted = value.IsDeleted,
IndexedAppId = value.AppId.Id,
IndexedSchemaId = value.SchemaId.Id,

4
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
public void Create(CreateApp command)
{
var appId = new NamedId<Guid>(command.AppId, command.Name);
var appId = NamedId.Of(command.AppId, command.Name);
var events = new List<AppEvent>
{
@ -308,7 +308,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
if (@event.AppId == null)
{
@event.AppId = new NamedId<Guid>(Snapshot.Id, Snapshot.Name);
@event.AppId = NamedId.Of(Snapshot.Id, Snapshot.Name);
}
RaiseEvent(Envelope.Create(@event));

6
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = new NamedId<Guid>(createApp.AppId, createApp.Name);
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task>(command =>
{
@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name);
var schemaId = NamedId.Of(command.SchemaId, command.Name);
await publish(new ConfigureScripts
{
@ -222,7 +222,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name);
var schemaId = NamedId.Of(command.SchemaId, command.Name);
await publish(new ConfigureScripts
{

14
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateProfileCommandMiddleware.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = new NamedId<Guid>(createApp.AppId, createApp.Name);
var appId = NamedId.Of(createApp.AppId, createApp.Name);
var publish = new Func<ICommand, Task>(command =>
{
@ -233,7 +233,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
return NamedId.Of(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateProjectsSchemaAsync(Func<ICommand, Task> publish)
@ -324,7 +324,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
return NamedId.Of(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateExperienceSchemaAsync(Func<ICommand, Task> publish)
@ -404,7 +404,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
return NamedId.Of(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateEducationSchemaAsync(Func<ICommand, Task> publish)
@ -484,7 +484,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
return NamedId.Of(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreatePublicationsSchemaAsync(Func<ICommand, Task> publish)
@ -552,7 +552,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
return NamedId.Of(command.SchemaId, command.Name);
}
private async Task<NamedId<Guid>> CreateSkillsSchemaAsync(Func<ICommand, Task> publish)
@ -597,7 +597,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command);
return new NamedId<Guid>(command.SchemaId, command.Name);
return NamedId.Of(command.SchemaId, command.Name);
}
}
}

6
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -154,12 +154,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Data = scriptEngine.Transform(new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }, scriptText);
}
result.Data = result.Data.Convert(schema.SchemaDef, converters);
result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters);
}
if (result.DataDraft != null)
{
result.DataDraft = result.DataDraft.Convert(schema.SchemaDef, converters);
result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
yield return result;
@ -172,11 +172,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!context.IsFrontendClient)
{
yield return FieldConverters.ExcludeHidden();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden());
}
if (checkType)
{
yield return FieldConverters.ExcludeChangedTypes();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes());
}
yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig);

7
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -12,7 +12,6 @@ using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
@ -20,7 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery;
private readonly ICommandBus commandBus;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository;
private readonly IAppProvider appProvider;
@ -28,20 +26,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public CachingGraphQLService(IMemoryCache cache,
IAppProvider appProvider,
IAssetRepository assetRepository,
ICommandBus commandBus,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(cache)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider;
this.assetRepository = assetRepository;
this.commandBus = commandBus;
this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator;
}
@ -58,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var modelContext = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, assetRepository, commandBus, contentQuery, urlGenerator);
var ctx = new GraphQLExecutionContext(context, assetRepository, contentQuery, urlGenerator);
return await modelContext.ExecuteAsync(ctx, query);
}

6
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -11,25 +11,19 @@ using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class GraphQLExecutionContext : QueryExecutionContext
{
public ICommandBus CommandBus { get; }
public IGraphQLUrlGenerator UrlGenerator { get; }
public GraphQLExecutionContext(QueryContext context,
IAssetRepository assetRepository,
ICommandBus commandBus,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(context, assetRepository, contentQuery)
{
CommandBus = commandBus;
UrlGenerator = urlGenerator;
}

34
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -12,12 +12,13 @@ using System.Threading.Tasks;
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using GraphQLSchema = GraphQL.Types.Schema;
@ -28,13 +29,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class GraphQLModel : IGraphModel
{
private readonly QueryGraphTypeVisitor schemaTypes;
private readonly Dictionary<ISchemaEntity, ContentGraphType> contentTypes = new Dictionary<ISchemaEntity, ContentGraphType>();
private readonly Dictionary<ISchemaEntity, ContentDataGraphType> contentDataTypes = new Dictionary<ISchemaEntity, ContentDataGraphType>();
private readonly Dictionary<Guid, ISchemaEntity> schemasById;
private readonly PartitionResolver partitionResolver;
private readonly IAppEntity app;
private readonly IGraphType assetType;
private readonly IGraphType assetListType;
private readonly GraphQLSchema graphQLSchema;
public bool CanGenerateAssetSourceUrl { get; private set; }
@ -48,10 +49,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl;
assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType));
schemasById = schemas.ToDictionary(x => x.Id);
schemaTypes = new QueryGraphTypeVisitor(GetContentType, new ListGraphType(new NonNullGraphType(assetType)));
graphQLSchema = BuildSchema(this);
graphQLSchema.RegisterValueConverter(JsonConverter.Instance);
InitializeContentTypes();
}
@ -60,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var schemas = model.schemasById.Values;
return new GraphQLSchema { Query = new AppQueriesGraphType(model, schemas), Mutation = new AppMutationsGraphType(model, schemas) };
return new GraphQLSchema { Query = new AppQueriesGraphType(model, schemas) };
}
private void InitializeContentTypes()
@ -78,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(IGraphType type)
{
return (type, new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName)));
return (type, new FuncFieldResolver<IReadOnlyDictionary<string, JToken>, object>(c => c.Source.GetOrDefault(c.FieldName)));
}
public IFieldResolver ResolveAssetUrl()
@ -134,14 +137,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key);
}
public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(IField field)
public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field)
{
return field.Accept(schemaTypes);
}
public IGraphType GetInputGraphType(IField field)
{
return field.GetInputGraphType();
return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType));
}
public IGraphType GetAssetType()
@ -158,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return null;
}
return schema != null ? contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType()) : null;
return schema != null ? contentDataTypes.GetOrAddNew(schema) : null;
}
public IGraphType GetContentType(Guid schemaId)
@ -170,6 +168,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return null;
}
contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType());
return contentTypes.GetOrAdd(schema, s => new ContentGraphType());
}
@ -177,13 +177,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
Guard.NotNull(context, nameof(context));
var inputs = query.Variables?.ToInputs();
var result = await new DocumentExecuter().ExecuteAsync(options =>
{
options.Inputs = query.Variables?.ToInputs() ?? new Inputs();
options.Query = query.Query;
options.OperationName = query.OperationName;
options.Schema = graphQLSchema;
options.UserContext = context;
options.Schema = graphQLSchema;
options.Inputs = inputs;
options.Query = query.Query;
}).ConfigureAwait(false);
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray());

2
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs

@ -9,7 +9,7 @@ using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public class GraphQLQuery
public sealed class GraphQLQuery
{
public string OperationName { get; set; }

5
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs

@ -10,6 +10,7 @@ using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
@ -34,8 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IGraphType GetContentDataType(Guid schemaId);
IGraphType GetInputGraphType(IField field);
(IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(IField field);
(IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field);
}
}

44
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs

@ -22,52 +22,46 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType Date = new DateGraphType();
public static readonly IGraphType Json = new JsonGraphType();
public static readonly IGraphType Tags = new ListGraphType<NonNullGraphType<StringGraphType>>();
public static readonly IGraphType Float = new FloatGraphType();
public static readonly IGraphType Status = new EnumerationGraphType<Status>();
public static readonly IGraphType String = new StringGraphType();
public static readonly IGraphType Boolean = new BooleanGraphType();
public static readonly IGraphType StatusType = new EnumerationGraphType<Status>();
public static readonly IGraphType References = new ListGraphType<NonNullGraphType<GuidGraphType>>();
public static readonly IGraphType NonNullInt = new NonNullGraphType(new IntGraphType());
public static readonly IGraphType NonNullInt = new NonNullGraphType(Int);
public static readonly IGraphType NonNullGuid = new NonNullGraphType(new GuidGraphType());
public static readonly IGraphType NonNullGuid = new NonNullGraphType(Guid);
public static readonly IGraphType NonNullDate = new NonNullGraphType(new DateGraphType());
public static readonly IGraphType NonNullDate = new NonNullGraphType(Date);
public static readonly IGraphType NonNullFloat = new NonNullGraphType(new FloatGraphType());
public static readonly IGraphType NonNullFloat = new NonNullGraphType(Float);
public static readonly IGraphType NonNullString = new NonNullGraphType(new StringGraphType());
public static readonly IGraphType NonNullString = new NonNullGraphType(String);
public static readonly IGraphType NonNullBoolean = new NonNullGraphType(new BooleanGraphType());
public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean);
public static readonly IGraphType NonNullStatusType = new NonNullGraphType(new EnumerationGraphType<Status>());
public static readonly IGraphType NonNullStatusType = new NonNullGraphType(Status);
public static readonly IGraphType ListOfNonNullGuid = new ListGraphType(new NonNullGraphType(new GuidGraphType()));
public static readonly IGraphType NoopDate = new NoopGraphType(Date);
public static readonly IGraphType ListOfNonNullString = new ListGraphType(new NonNullGraphType(new StringGraphType()));
public static readonly IGraphType NoopJson = new NoopGraphType(Json);
public static readonly IGraphType NoopInt = new NoopGraphType("Int");
public static readonly IGraphType NoopFloat = new NoopGraphType(Float);
public static readonly IGraphType NoopGuid = new NoopGraphType("Guid");
public static readonly IGraphType NoopString = new NoopGraphType(String);
public static readonly IGraphType NoopDate = new NoopGraphType("Date");
public static readonly IGraphType NoopJson = new NoopGraphType("Json");
public static readonly IGraphType NoopBoolean = new NoopGraphType(Boolean);
public static readonly IGraphType NoopTags = new NoopGraphType("Tags");
public static readonly IGraphType NoopFloat = new NoopGraphType("Float");
public static readonly IGraphType NoopString = new NoopGraphType("String");
public static readonly IGraphType NoopBoolean = new NoopGraphType("Boolean");
public static readonly IGraphType NoopGeolocation = new NoopGraphType("Geolocation");
public static readonly IGraphType CommandVersion = new CommandVersionGraphType();
public static readonly IGraphType GeolocationInput = new GeolocationInputGraphType();
}
}

344
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs

@ -1,344 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class AppMutationsGraphType : ObjectGraphType
{
public AppMutationsGraphType(IGraphModel model, IEnumerable<ISchemaEntity> schemas)
{
foreach (var schema in schemas)
{
var schemaId = schema.NamedId();
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
var contentType = model.GetContentType(schema.Id);
var contentDataType = model.GetContentDataType(schema.Id);
var resultType = new ContentDataChangedResultGraphType(schemaType, schemaName, contentDataType);
var inputType = new ContentDataGraphInputType(model, schema);
AddContentCreate(schemaId, schemaType, schemaName, inputType, contentDataType, contentType);
AddContentUpdate(schemaType, schemaName, inputType, resultType);
AddContentPatch(schemaType, schemaName, inputType, resultType);
AddContentPublish(schemaType, schemaName);
AddContentUnpublish(schemaType, schemaName);
AddContentArchive(schemaType, schemaName);
AddContentRestore(schemaType, schemaName);
AddContentDelete(schemaType, schemaName);
}
Description = "The app mutations.";
}
private void AddContentCreate(NamedId<Guid> schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType contentDataType, IGraphType contentType)
{
AddField(new FieldType
{
Name = $"create{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(contentType),
Resolver = ResolveAsync(async (c, publish) =>
{
var argPublish = c.GetArgument<bool>("publish");
var contentData = GetContentData(c);
var command = new CreateContent { SchemaId = schemaId, Data = contentData, Publish = argPublish };
var commandContext = await publish(command);
var result = commandContext.Result<EntityCreatedResult<NamedContentData>>();
var response = ContentEntity.Create(command, result);
return (IContentEntity)ContentEntity.Create(command, result);
}),
Description = $"Creates an {schemaName} content."
});
}
private void AddContentUpdate(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType)
{
AddField(new FieldType
{
Name = $"update{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(resultType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new UpdateContent { ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result;
}),
Description = $"Update an {schemaName} content by id."
});
}
private void AddContentPatch(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType)
{
AddField(new FieldType
{
Name = $"patch{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(resultType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new PatchContent { ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result;
}),
Description = $"Patch a {schemaName} content."
});
}
private void AddContentPublish(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"publish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Published };
return publish(command);
}),
Description = $"Publish a {schemaName} content."
});
}
private void AddContentUnpublish(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"unpublish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Unpublish a {schemaName} content."
});
}
private void AddContentArchive(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"archive{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Archived };
return publish(command);
}),
Description = $"Archive a {schemaName} content."
});
}
private void AddContentRestore(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"restore{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Restore a {schemaName} content."
});
}
private void AddContentDelete(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"delete{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new DeleteContent { ContentId = contentId };
return publish(command);
}),
Description = $"Delete an {schemaName} content."
});
}
private static QueryArguments CreateIdArguments(string schemaName)
{
return new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
private static IFieldResolver ResolveAsync<T>(Func<ResolveFieldContext, Func<SquidexCommand, Task<CommandContext>>, Task<T>> action)
{
return new FuncFieldResolver<Task<T>>(async c =>
{
var e = (GraphQLExecutionContext)c.UserContext;
try
{
return await action(c, command =>
{
command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any);
return e.CommandBus.PublishAsync(command);
});
}
catch (ValidationException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
catch (DomainException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
});
}
private static NamedContentData GetContentData(ResolveFieldContext c)
{
return JObject.FromObject(c.GetArgument<object>("data")).ToObject<NamedContentData>();
}
}
}

44
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/CommandVersionGraphType.cs

@ -1,44 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class CommandVersionGraphType : ObjectGraphType<CommandContext>
{
public CommandVersionGraphType()
{
Name = "CommandVersionDto";
AddField(new FieldType
{
Name = "version",
ResolvedType = AllTypes.Int,
Resolver = ResolveVersion(),
Description = "The new version of the item."
});
Description = "The result of a mutation";
}
private static IFieldResolver ResolveVersion()
{
return new FuncFieldResolver<CommandContext, int?>(x =>
{
if (x.Source.Result<object>() is EntitySavedResult result)
{
return (int)result.Version;
}
return null;
});
}
}
}

44
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataChangedResultGraphType.cs

@ -1,44 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentDataChangedResultGraphType : ObjectGraphType<ContentDataChangedResult>
{
public ContentDataChangedResultGraphType(string schemaType, string schemaName, IGraphType contentDataType)
{
Name = $"{schemaName}DataChangedResultDto";
AddField(new FieldType
{
Name = "version",
ResolvedType = AllTypes.Int,
Resolver = Resolve(x => x.Version),
Description = $"The new version of the {schemaName} content."
});
AddField(new FieldType
{
Name = "data",
ResolvedType = new NonNullGraphType(contentDataType),
Resolver = Resolve(x => x.Data),
Description = $"The new data of the {schemaName} content."
});
Description = $"The result of the {schemaName} mutation";
}
private static IFieldResolver Resolve(Func<ContentDataChangedResult, object> action)
{
return new FuncFieldResolver<ContentDataChangedResult, object>(c => action(c.Source));
}
}
}

74
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphInputType.cs

@ -1,74 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentDataGraphInputType : InputObjectGraphType
{
public ContentDataGraphInputType(IGraphModel model, ISchemaEntity schema)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
Name = $"{schemaType}InputDto";
foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden))
{
var inputType = model.GetInputGraphType(field);
if (inputType != null)
{
if (field.RawProperties.IsRequired)
{
inputType = new NonNullGraphType(inputType);
}
var fieldName = field.RawProperties.Label.WithFallback(field.Name);
var fieldGraphType = new InputObjectGraphType
{
Name = $"{schemaType}Data{field.Name.ToPascalCase()}InputDto"
};
var partition = model.ResolvePartition(field.Partitioning);
foreach (var partitionItem in partition)
{
fieldGraphType.AddField(new FieldType
{
Name = partitionItem.Key,
ResolvedType = inputType,
Resolver = null,
Description = field.RawProperties.Hints
});
}
fieldGraphType.Description = $"The input structure of the {fieldName} of a {schemaName} content type.";
var fieldResolver = new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(field.Name));
AddField(new FieldType
{
Name = field.Name.ToCamelCase(),
Resolver = fieldResolver,
ResolvedType = fieldGraphType,
Description = $"The {fieldName} field."
});
}
}
Description = $"The structure of a {schemaName} content type.";
}
}
}

32
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -5,9 +5,11 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using GraphQL.Resolvers;
using GraphQL.Types;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
@ -25,33 +27,49 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden))
{
var fieldInfo = model.GetGraphType(field);
var fieldInfo = model.GetGraphType(schema, field);
if (fieldInfo.ResolveType != null)
{
var fieldName = field.RawProperties.Label.WithFallback(field.Name);
var fieldType = field.TypeName();
var fieldName = field.DisplayName();
var fieldGraphType = new ObjectGraphType
{
Name = $"{schemaType}Data{field.Name.ToPascalCase()}Dto"
Name = $"{schemaType}Data{fieldType}Dto"
};
var partition = model.ResolvePartition(field.Partitioning);
foreach (var partitionItem in partition)
{
var resolver = new FuncFieldResolver<object>(c =>
{
if (((ContentFieldData)c.Source).TryGetValue(c.FieldName, out var value))
{
return fieldInfo.Resolver(value, c);
}
else
{
return fieldInfo;
}
});
fieldGraphType.AddField(new FieldType
{
Name = partitionItem.Key,
Resolver = fieldInfo.Resolver,
Resolver = resolver,
ResolvedType = fieldInfo.ResolveType,
Description = field.RawProperties.Hints
});
}
fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type.";
fieldGraphType.Description = $"The structure of the {fieldName} field of the {schemaName} content type.";
var fieldResolver = new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(field.Name));
var fieldResolver = new FuncFieldResolver<NamedContentData, IReadOnlyDictionary<string, JToken>>(c =>
{
return c.Source.GetOrDefault(field.Name);
});
AddField(new FieldType
{
@ -63,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
}
Description = $"The structure of a {schemaName} content type.";
Description = $"The structure of the {schemaName} content type.";
}
}
}

31
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GeolocationInputGraphType.cs

@ -1,31 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class GeolocationInputGraphType : InputObjectGraphType
{
public GeolocationInputGraphType()
{
Name = "GeolocationInputDto";
AddField(new FieldType
{
Name = "latitude",
ResolvedType = AllTypes.NonNullFloat
});
AddField(new FieldType
{
Name = "longitude",
ResolvedType = AllTypes.NonNullFloat
});
}
}
}

66
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs

@ -1,66 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class InputFieldVisitor : IFieldVisitor<IGraphType>
{
public static readonly InputFieldVisitor Default = new InputFieldVisitor();
private InputFieldVisitor()
{
}
public IGraphType Visit(IField<AssetsFieldProperties> field)
{
return AllTypes.ListOfNonNullGuid;
}
public IGraphType Visit(IField<BooleanFieldProperties> field)
{
return AllTypes.Boolean;
}
public IGraphType Visit(IField<DateTimeFieldProperties> field)
{
return AllTypes.Date;
}
public IGraphType Visit(IField<GeolocationFieldProperties> field)
{
return AllTypes.GeolocationInput;
}
public IGraphType Visit(IField<JsonFieldProperties> field)
{
return AllTypes.NoopJson;
}
public IGraphType Visit(IField<NumberFieldProperties> field)
{
return AllTypes.Float;
}
public IGraphType Visit(IField<ReferencesFieldProperties> field)
{
return AllTypes.ListOfNonNullGuid;
}
public IGraphType Visit(IField<StringFieldProperties> field)
{
return AllTypes.String;
}
public IGraphType Visit(IField<TagsFieldProperties> field)
{
return AllTypes.ListOfNonNullString;
}
}
}

61
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using GraphQL.Resolvers;
using GraphQL.Types;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class NestedGraphType : ObjectGraphType<JObject>
{
public NestedGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
var fieldType = field.TypeName();
var fieldName = field.DisplayName();
Name = $"{schemaType}{fieldName}ChildDto";
foreach (var nestedField in field.Fields.Where(x => !x.IsHidden))
{
var fieldInfo = model.GetGraphType(schema, nestedField);
if (fieldInfo.ResolveType != null)
{
var resolver = new FuncFieldResolver<object>(c =>
{
if (((JObject)c.Source).TryGetValue(nestedField.Name, out var value))
{
return fieldInfo.Resolver(value, c);
}
else
{
return fieldInfo;
}
});
AddField(new FieldType
{
Name = nestedField.Name.ToCamelCase(),
Resolver = resolver,
ResolvedType = fieldInfo.ResolveType,
Description = $"The {fieldName}/{nestedField.DisplayName()} nested field."
});
}
}
Description = $"The structure of the {schemaName}.{fieldName} nested schema.";
}
}
}

66
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs

@ -6,89 +6,106 @@
// ==========================================================================
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType ResolveType, IFieldResolver Resolver)>
public delegate object ValueResolver(JToken value, ResolveFieldContext context);
public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType ResolveType, ValueResolver Resolver)>
{
private static readonly ValueResolver NoopResolver = new ValueResolver((value, c) => value);
private readonly ISchemaEntity schema;
private readonly Func<Guid, IGraphType> schemaResolver;
private readonly IGraphModel model;
private readonly IGraphType assetListType;
public QueryGraphTypeVisitor(Func<Guid, IGraphType> schemaResolver, IGraphType assetListType)
public QueryGraphTypeVisitor(ISchemaEntity schema, Func<Guid, IGraphType> schemaResolver, IGraphModel model, IGraphType assetListType)
{
this.model = model;
this.assetListType = assetListType;
this.schema = schema;
this.schemaResolver = schemaResolver;
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<AssetsFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IArrayField field)
{
return ResolveNested(field);
}
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<AssetsFieldProperties> field)
{
return ResolveAssets(assetListType);
return ResolveAssets();
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<BooleanFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<BooleanFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopBoolean);
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<DateTimeFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<DateTimeFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopDate);
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<GeolocationFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<GeolocationFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopGeolocation);
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<JsonFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<JsonFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopJson);
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<NumberFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<NumberFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopFloat);
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<ReferencesFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<ReferencesFieldProperties> field)
{
return ResolveReferences(field);
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<StringFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<StringFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopString);
}
public (IGraphType ResolveType, IFieldResolver Resolver) Visit(IField<TagsFieldProperties> field)
public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField<TagsFieldProperties> field)
{
return ResolveDefault(AllTypes.NoopTags);
}
private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(IGraphType type)
private static (IGraphType ResolveType, ValueResolver Resolver) ResolveDefault(IGraphType type)
{
return (type, new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName)));
return (type, NoopResolver);
}
private (IGraphType ResolveType, ValueResolver Resolver) ResolveNested(IArrayField field)
{
var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field)));
return (schemaFieldType, NoopResolver);
}
private static ValueTuple<IGraphType, IFieldResolver> ResolveAssets(IGraphType assetListType)
private (IGraphType ResolveType, ValueResolver Resolver) ResolveAssets()
{
var resolver = new FuncFieldResolver<ContentFieldData, object>(c =>
var resolver = new ValueResolver((value, c) =>
{
var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedAssetsAsync(contentIds);
return context.GetReferencedAssetsAsync(value);
});
return (assetListType, resolver);
}
private ValueTuple<IGraphType, IFieldResolver> ResolveReferences(IField field)
private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field)
{
var schemaId = ((ReferencesFieldProperties)field.RawProperties).SchemaId;
@ -99,12 +116,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return (null, null);
}
var resolver = new FuncFieldResolver<ContentFieldData, object>(c =>
var resolver = new ValueResolver((value, c) =>
{
var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedContentsAsync(schemaId, contentIds);
return context.GetReferencedContentsAsync(schemaId, value);
});
var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType));

2
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GuidGraphType.cs → src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType.cs

@ -9,7 +9,7 @@ using System;
using GraphQL.Language.AST;
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{
public sealed class GuidGraphType : ScalarGraphType
{

32
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Language.AST;
using GraphQL.Types;
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{
public sealed class JsonConverter : IAstFromValueConverter
{
public static readonly JsonConverter Instance = new JsonConverter();
private JsonConverter()
{
}
public IValue Convert(object value, IGraphType type)
{
return new JsonValue(value as JObject);
}
public bool Matches(object value, IGraphType type)
{
return value is JObject;
}
}
}

42
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs

@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Language.AST;
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{
public sealed class JsonGraphType : ScalarGraphType
{
public JsonGraphType()
{
Name = "Json";
Description = "Unstructured Json object";
}
public override object Serialize(object value)
{
return value;
}
public override object ParseValue(object value)
{
return value;
}
public override object ParseLiteral(IValue value)
{
if (value is JsonValue jsonGraphType)
{
return jsonGraphType.Value;
}
return value;
}
}
}

25
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValue.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Language.AST;
using Newtonsoft.Json.Linq;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{
public sealed class JsonValue : ValueNode<JObject>
{
public JsonValue(JObject value)
{
Value = value;
}
protected override bool Equals(ValueNode<JObject> node)
{
return false;
}
}
}

9
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL.Language.AST;
using GraphQL.Types;
@ -18,6 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
Name = name;
}
public NoopGraphType(IGraphType type)
: this(type.Name)
{
Description = type.Description;
}
public override object Serialize(object value)
{
return value;
@ -30,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
public override object ParseLiteral(IValue value)
{
throw new NotSupportedException();
return value.Value;
}
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs

@ -11,6 +11,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class AddField : SchemaCommand
{
public long? ParentFieldId { get; set; }
public string Name { get; set; }
public string Partitioning { get; set; }

3
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using FieldNested = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.CreateSchemaNestedField>;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
@ -21,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
public bool IsDisabled { get; set; }
public FieldNested Nested { get; set; }
public FieldProperties Properties { get; set; }
}
}

22
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaNestedField.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class CreateSchemaNestedField
{
public string Name { get; set; }
public bool IsHidden { get; set; }
public bool IsDisabled { get; set; }
public FieldProperties Properties { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs

@ -9,6 +9,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public class FieldCommand : SchemaCommand
{
public long? ParentFieldId { get; set; }
public long FieldId { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs

@ -11,6 +11,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class ReorderFields : SchemaCommand
{
public long? ParentFieldId { get; set; }
public List<long> FieldIds { get; set; }
}
}

13
src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
@ -22,7 +21,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
public static IEnumerable<ValidationError> Validate(FieldProperties properties)
{
return properties?.Accept(Instance) ?? Enumerable.Empty<ValidationError>();
return properties?.Accept(Instance);
}
public IEnumerable<ValidationError> Visit(ArrayFieldProperties properties)
{
if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value >= properties.MaxItems.Value)
{
yield return new ValidationError("Max items must be greater than min items.",
nameof(properties.MinItems),
nameof(properties.MaxItems));
}
}
public IEnumerable<ValidationError> Visit(AssetsFieldProperties properties)

89
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
@ -32,12 +34,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name)));
}
if (command.Fields != null && command.Fields.Any())
if (command.Fields?.Count > 0)
{
var index = 0;
foreach (var field in command.Fields)
{
index++;
var prefix = $"Fields.{index}";
if (!field.Partitioning.IsValidPartitioning())
@ -54,12 +58,57 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
error(new ValidationError("Properties is required.", $"{prefix}.{nameof(field.Properties)}"));
}
else
{
var errors = FieldPropertiesValidator.Validate(field.Properties);
var propertyErrors = FieldPropertiesValidator.Validate(field.Properties);
foreach (var e in errors)
{
error(e.WithPrefix(prefix));
}
}
foreach (var propertyError in propertyErrors)
if (field.Nested?.Count > 0)
{
error(propertyError);
if (!(field.Properties is ArrayFieldProperties))
{
error(new ValidationError("Only array fields can have nested fields.", $"{prefix}.{nameof(field.Partitioning)}"));
}
else
{
var nestedIndex = 0;
foreach (var nestedField in field.Nested)
{
nestedIndex++;
var nestedPrefix = $"Fields.{index}.Nested.{nestedIndex}";
if (!nestedField.Name.IsPropertyName())
{
error(new ValidationError("Name must be a valid property name.", $"{prefix}.{nameof(nestedField.Name)}"));
}
if (nestedField.Properties == null)
{
error(new ValidationError("Properties is required.", $"{prefix}.{nameof(nestedField.Properties)}"));
}
else
{
var errors = FieldPropertiesValidator.Validate(nestedField.Properties);
foreach (var e in errors)
{
error(e.WithPrefix(nestedPrefix));
}
}
}
}
if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count)
{
error(new ValidationError("Fields cannot have duplicate names.", $"{prefix}.Nested"));
}
}
}
@ -75,6 +124,22 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
IArrayField arrayField = null;
if (command.ParentFieldId.HasValue)
{
var parentId = command.ParentFieldId.Value;
if (schema.FieldsById.TryGetValue(parentId, out var field) && field is IArrayField a)
{
arrayField = a;
}
else
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
}
Validate.It(() => "Cannot reorder schema fields.", error =>
{
if (command.FieldIds == null)
@ -82,13 +147,25 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
error(new ValidationError("Field ids is required.", nameof(command.FieldIds)));
}
if (command.FieldIds != null && (command.FieldIds.Count != schema.Fields.Count || command.FieldIds.Any(x => !schema.FieldsById.ContainsKey(x))))
if (arrayField == null)
{
error(new ValidationError("Ids must cover all fields.", nameof(command.FieldIds)));
CheckFields(error, command, schema.FieldsById);
}
else
{
CheckFields(error, command, arrayField.FieldsById);
}
});
}
private static void CheckFields<T>(Action<ValidationError> error, ReorderFields c, IReadOnlyDictionary<long, T> fields)
{
if (c.FieldIds != null && (c.FieldIds.Count != fields.Count || c.FieldIds.Any(x => !fields.ContainsKey(x))))
{
error(new ValidationError("Ids must cover all fields.", nameof(c.FieldIds)));
}
}
public static void CanPublish(Schema schema, PublishSchema command)
{
Guard.NotNull(command, nameof(command));

122
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs

@ -20,11 +20,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
Validate.It(() => "Cannot add a new field.", error =>
{
if (!command.Partitioning.IsValidPartitioning())
{
error(new ValidationError("Partitioning is not valid.", nameof(command.Partitioning)));
}
if (!command.Name.IsPropertyName())
{
error(new ValidationError("Name must be a valid property name.", nameof(command.Name)));
@ -34,17 +29,40 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
error(new ValidationError("Properties is required.", nameof(command.Properties)));
}
else
{
var errors = FieldPropertiesValidator.Validate(command.Properties);
var propertyErrors = FieldPropertiesValidator.Validate(command.Properties);
foreach (var e in errors)
{
error(e.WithPrefix(nameof(command.Properties)));
}
}
foreach (var propertyError in propertyErrors)
if (command.ParentFieldId.HasValue)
{
error(propertyError);
var parentId = command.ParentFieldId.Value;
if (!schema.FieldsById.TryGetValue(parentId, out var rootField) || !(rootField is IArrayField arrayField))
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
if (arrayField.FieldsByName.ContainsKey(command.Name))
{
error(new ValidationError($"There is already a field with name '{command.Name}'", nameof(command.Name)));
}
}
if (schema.FieldsByName.ContainsKey(command.Name))
else
{
error(new ValidationError($"There is already a field with name '{command.Name}'", nameof(command.Name)));
if (command.ParentFieldId == null && !command.Partitioning.IsValidPartitioning())
{
error(new ValidationError("Partitioning is not valid.", nameof(command.Partitioning)));
}
if (schema.FieldsByName.ContainsKey(command.Name))
{
error(new ValidationError($"There is already a field with name '{command.Name}'", nameof(command.Name)));
}
}
});
}
@ -53,80 +71,89 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
throw new DomainException("Schema field is already locked.");
}
Validate.It(() => "Cannot update field.", error =>
{
if (command.Properties == null)
{
error(new ValidationError("Properties is required.", nameof(command.Properties)));
}
var propertyErrors = FieldPropertiesValidator.Validate(command.Properties);
foreach (var propertyError in propertyErrors)
else
{
error(propertyError);
var errors = FieldPropertiesValidator.Validate(command.Properties);
foreach (var e in errors)
{
error(e.WithPrefix(nameof(command.Properties)));
}
}
});
var field = GetFieldOrThrow(schema, command.FieldId);
if (field.IsLocked)
{
throw new DomainException("Schema field is already locked.");
}
}
public static void CanDelete(Schema schema, DeleteField command)
public static void CanHide(Schema schema, HideField command)
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId);
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
throw new DomainException("Schema field is locked.");
}
if (field.IsHidden)
{
throw new DomainException("Schema field is already hidden.");
}
}
public static void CanHide(Schema schema, HideField command)
public static void CanDisable(Schema schema, DisableField command)
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId);
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsHidden)
if (field.IsDisabled)
{
throw new DomainException("Schema field is already hidden.");
throw new DomainException("Schema field is already disabled.");
}
}
public static void CanShow(Schema schema, ShowField command)
public static void CanDelete(Schema schema, DeleteField command)
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId);
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (!field.IsHidden)
if (field.IsLocked)
{
throw new DomainException("Schema field is already visible.");
throw new DomainException("Schema field is locked.");
}
}
public static void CanDisable(Schema schema, DisableField command)
public static void CanShow(Schema schema, ShowField command)
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId);
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsDisabled)
if (!field.IsHidden)
{
throw new DomainException("Schema field is already disabled.");
throw new DomainException("Schema field is already visible.");
}
}
public static void CanEnable(Schema schema, EnableField command)
{
var field = GetFieldOrThrow(schema, command.FieldId);
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (!field.IsDisabled)
{
@ -138,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{
Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId);
var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked)
{
@ -146,8 +173,23 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
}
}
private static Field GetFieldOrThrow(Schema schema, long fieldId)
private static IField GetFieldOrThrow(Schema schema, long fieldId, long? parentId)
{
if (parentId.HasValue)
{
if (!schema.FieldsById.TryGetValue(parentId.Value, out var rootField) || !(rootField is IArrayField arrayField))
{
throw new DomainObjectNotFoundException(parentId.ToString(), "Fields", typeof(Schema));
}
if (!arrayField.FieldsById.TryGetValue(fieldId, out var nestedField))
{
throw new DomainObjectNotFoundException(fieldId.ToString(), $"Fields[{parentId}].Fields", typeof(Schema));
}
return nestedField;
}
if (!schema.FieldsById.TryGetValue(fieldId, out var field))
{
throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema));

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

Loading…
Cancel
Save