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) # Scripts (should be copied from node_modules on build)
**/wwwroot/scripts/**/*.* **/wwwroot/scripts/**/*.*
/src/Squidex/appsettings.Development.json
/src/Squidex/Assets /src/Squidex/Assets
/src/Squidex/package-lock.json
/src/Squidex/Properties/launchSettings.json
/src/Squidex/appsettings.Development.json /global.json
/src/Squidex/Properties/launchSettings.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) protected ContentData(int capacity, IEqualityComparer<T> comparer)
: base(copy, comparer) : base(capacity, comparer)
{ {
} }
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
foreach (var otherValue in source) 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) 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) public IdContentData(int capacity)
: base(copy, EqualityComparer<long>.Default) : 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) public NamedContentData(int capacity)
: base(copy, EqualityComparer<string>.Default) : 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; 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); 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); 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); 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 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 public sealed class FieldRegistry
{ {
private delegate Field FactoryFunction(long id, string name, Partitioning partitioning, FieldProperties properties);
private readonly TypeNameRegistry typeNameRegistry; 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) public FieldRegistry(TypeNameRegistry typeNameRegistry)
{ {
@ -38,23 +36,34 @@ namespace Squidex.Domain.Apps.Core.Schemas
private void RegisterField(Type type) 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."); 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public static class Fields 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) 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)); 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); 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; } string Name { get; }
bool IsLocked { get; }
bool IsDisabled { get; } bool IsDisabled { get; }
bool IsHidden { get; } bool IsHidden { get; }
bool IsLocked { get; }
FieldProperties RawProperties { get; } FieldProperties RawProperties { get; }
T Accept<T>(IFieldVisitor<T> visitor); 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> public interface IFieldPropertiesVisitor<out T>
{ {
T Visit(ArrayFieldProperties properties);
T Visit(AssetsFieldProperties properties); T Visit(AssetsFieldProperties properties);
T Visit(BooleanFieldProperties 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> public interface IFieldVisitor<out T>
{ {
T Visit(IArrayField field);
T Visit(IField<AssetsFieldProperties> field); T Visit(IField<AssetsFieldProperties> field);
T Visit(IField<BooleanFieldProperties> 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json namespace Squidex.Domain.Apps.Core.Schemas.Json
@ -15,21 +16,24 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
public long Id { get; set; } public long Id { get; set; }
[JsonProperty] [JsonProperty]
public bool IsHidden { get; set; } public string Name { get; set; }
[JsonProperty] [JsonProperty]
public bool IsLocked { get; set; } public string Partitioning { get; set; }
[JsonProperty] [JsonProperty]
public bool IsDisabled { get; set; } public bool IsHidden { get; set; }
[JsonProperty] [JsonProperty]
public string Name { get; set; } public bool IsLocked { get; set; }
[JsonProperty] [JsonProperty]
public string Partitioning { get; set; } public bool IsDisabled { get; set; }
[JsonProperty] [JsonProperty]
public FieldProperties Properties { get; set; } 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 public sealed class JsonSchemaModel
{ {
private static readonly Field[] Empty = new Field[0]; private static readonly RootField[] Empty = new RootField[0];
[JsonProperty] [JsonProperty]
public string Name { get; set; } public string Name { get; set; }
@ -38,11 +38,12 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
Properties = schema.Properties; Properties = schema.Properties;
Fields = Fields =
schema.Fields?.Select(x => schema.Fields.Select(x =>
new JsonFieldModel new JsonFieldModel
{ {
Id = x.Id, Id = x.Id,
Name = x.Name, Name = x.Name,
Children = CreateChildren(x),
IsHidden = x.IsHidden, IsHidden = x.IsHidden,
IsLocked = x.IsLocked, IsLocked = x.IsLocked,
IsDisabled = x.IsDisabled, IsDisabled = x.IsDisabled,
@ -53,13 +54,31 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
IsPublished = schema.IsPublished; 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) if (Fields != null)
{ {
fields = new Field[Fields.Count]; fields = new RootField[Fields.Count];
for (var i = 0; i < fields.Length; i++) 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 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) 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); 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); 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); 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 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 long fieldId;
private readonly Partitioning partitioning;
private readonly string fieldName; private readonly string fieldName;
private readonly Partitioning partitioning;
private bool isDisabled; private bool isDisabled;
private bool isHidden; private bool isHidden;
private bool isLocked; private bool isLocked;
@ -51,11 +51,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
public abstract FieldProperties RawProperties { get; } 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.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(partitioning, nameof(partitioning));
Guard.GreaterThan(id, 0, nameof(id)); Guard.GreaterThan(id, 0, nameof(id));
Guard.NotNull(partitioning, nameof(partitioning));
fieldId = id; fieldId = id;
fieldName = name; fieldName = name;
@ -64,16 +64,16 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Field Lock() public RootField Lock()
{ {
return Clone<Field>(clone => return Clone(clone =>
{ {
clone.isLocked = true; clone.isLocked = true;
}); });
} }
[Pure] [Pure]
public Field Hide() public RootField Hide()
{ {
return Clone(clone => return Clone(clone =>
{ {
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Field Show() public RootField Show()
{ {
return Clone(clone => return Clone(clone =>
{ {
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Field Disable() public RootField Disable()
{ {
return Clone(clone => return Clone(clone =>
{ {
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Field Enable() public RootField Enable()
{ {
return Clone(clone => 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 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 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; private T properties;
@ -25,27 +25,31 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return properties; } 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) : base(id, name, partitioning)
{ {
Guard.NotNull(properties, nameof(properties)); Guard.NotNull(properties, nameof(properties));
this.properties = properties; SetProperties(properties);
this.properties.Freeze();
} }
[Pure] [Pure]
public override Field Update(FieldProperties newProperties) public override RootField Update(FieldProperties newProperties)
{ {
var typedProperties = ValidateProperties(newProperties); var typedProperties = ValidateProperties(newProperties);
return Clone<Field<T>>(clone => return Clone<RootField<T>>(clone =>
{ {
clone.properties = typedProperties; clone.SetProperties(typedProperties);
clone.properties.Freeze();
}); });
} }
private void SetProperties(T newProperties)
{
properties = newProperties;
properties.Freeze();
}
private T ValidateProperties(FieldProperties newProperties) private T ValidateProperties(FieldProperties newProperties)
{ {
Guard.NotNull(newProperties, nameof(newProperties)); Guard.NotNull(newProperties, nameof(newProperties));
@ -60,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public override TResult Accept<TResult>(IFieldVisitor<TResult> visitor) 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Contracts; using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
@ -17,9 +15,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public sealed class Schema : Cloneable<Schema> public sealed class Schema : Cloneable<Schema>
{ {
private readonly string name; private readonly string name;
private ImmutableArray<Field> fieldsOrdered = ImmutableArray<Field>.Empty; private FieldCollection<RootField> fields = FieldCollection<RootField>.Empty;
private ImmutableDictionary<long, Field> fieldsById;
private ImmutableDictionary<string, Field> fieldsByName;
private SchemaProperties properties; private SchemaProperties properties;
private bool isPublished; private bool isPublished;
@ -33,49 +29,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return isPublished; } 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 get { return fields.ById; }
{
if (fieldsById == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsById = ImmutableDictionary<long, Field>.Empty;
}
else
{
fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
}
}
return fieldsById;
}
} }
public IReadOnlyDictionary<string, Field> FieldsByName public IReadOnlyDictionary<string, RootField> FieldsByName
{ {
get get { return fields.ByName; }
{
if (fieldsByName == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsByName = ImmutableDictionary<string, Field>.Empty;
}
else
{
fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name);
}
}
return fieldsByName;
}
} }
public SchemaProperties Properties public SchemaProperties Properties
@ -93,21 +59,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.properties.Freeze(); 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) : this(name, properties)
{ {
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(fields, nameof(fields)); Guard.NotNull(fields, nameof(fields));
this.isPublished = isPublished; this.fields = new FieldCollection<RootField>(fields);
fieldsOrdered = ImmutableArray.Create(fields);
}
protected override void OnCloned() this.isPublished = isPublished;
{
fieldsById = null;
fieldsByName = null;
} }
[Pure] [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] [Pure]
public Schema Publish() public Schema Publish()
{ {
@ -197,62 +102,39 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure] [Pure]
public Schema DeleteField(long fieldId) public Schema DeleteField(long fieldId)
{ {
if (!FieldsById.TryGetValue(fieldId, out var field)) return Updatefields(f => f.Remove(fieldId));
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.Remove(field);
});
} }
[Pure] [Pure]
public Schema ReorderFields(List<long> ids) public Schema ReorderFields(List<long> ids)
{ {
Guard.NotNull(ids, nameof(ids)); return Updatefields(f => f.Reorder(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();
});
} }
[Pure] [Pure]
public Schema AddField(Field field) public Schema AddField(RootField field)
{ {
Guard.NotNull(field, nameof(field)); return Updatefields(f => f.Add(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);
});
} }
[Pure] [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 this;
} }
return Clone(clone => 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); 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); 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. // 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.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ConvertContent namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
public delegate ContentFieldData FieldConverter(ContentFieldData data, Field field);
public static class ContentConverter 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)); Guard.NotNull(schema, nameof(schema));
var result = new NamedContentData(); var result = new NamedContentData(content.Count);
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;
}
}
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)); Guard.NotNull(schema, nameof(schema));
var result = new IdContentData(); var result = new IdContentData(content.Count);
foreach (var fieldValue in content) return ConvertInternal(content, result, schema.FieldsById, KeyIdResolver, converters);
{
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;
} }
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)); Guard.NotNull(schema, nameof(schema));
var result = new IdContentData(); var result = new NamedContentData(content.Count);
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;
}
}
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)); 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; 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 (newvalue != null)
{
if (converters != null)
{
foreach (var converter in converters)
{ {
fieldData = converter(fieldData, field); target.Add(targetKey(field), newvalue);
if (fieldData == null)
{
break;
}
} }
} }
return fieldData; return target;
} }
} }
} }

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

@ -8,7 +8,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
@ -21,8 +20,17 @@ using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ConvertContent namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
public delegate ContentFieldData FieldConverter(ContentFieldData data, IRootField field);
public static class FieldConverters 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() public static FieldConverter ExcludeHidden()
{ {
return (data, field) => return (data, field) =>
@ -35,29 +43,23 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
return (data, field) => return (data, field) =>
{ {
var isValid = true;
foreach (var value in data.Values) foreach (var value in data.Values)
{ {
if (value.IsNull())
{
continue;
}
try try
{ {
if (!value.IsNull()) JsonValueConverter.ConvertValue(field, value);
{
JsonValueConverter.ConvertValue(field, value);
}
} }
catch catch
{ {
isValid = false; return null;
break;
} }
} }
if (!isValid)
{
return null;
}
return data; return data;
}; };
} }
@ -173,12 +175,10 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
return (data, field) => data; return (data, field) => data;
} }
var languageCodes = var languageCodes = languages.Select(x => x.Iso2Code).Where(x => languagesConfig.Contains(x));
new HashSet<string>( var languageSet = new HashSet<string>(languageCodes, StringComparer.OrdinalIgnoreCase);
languages.Select(x => x.Iso2Code).Where(x => languagesConfig.Contains(x)),
StringComparer.OrdinalIgnoreCase);
if (languageCodes.Count == 0) if (languageSet.Count == 0)
{ {
return (data, field) => data; return (data, field) => data;
} }
@ -189,7 +189,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{ {
var result = new ContentFieldData(); var result = new ContentFieldData();
foreach (var languageCode in languageCodes) foreach (var languageCode in languageSet)
{ {
if (data.TryGetValue(languageCode, out var value)) 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) => return (data, field) =>
{ {
if (field is IField<JsonFieldProperties>) if (field is IArrayField arrayField)
{ {
var result = new ContentFieldData(); 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; 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) => return (data, field) =>
{ {
if (field is IField<JsonFieldProperties>) if (!(field is IArrayField))
{ {
var result = new ContentFieldData(); 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 namespace Squidex.Domain.Apps.Core.EnrichContent
{ {
public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JToken> public sealed class DefaultValueFactory : IFieldVisitor<JToken>
{ {
private readonly Instant now; private readonly Instant now;
@ -26,62 +26,67 @@ namespace Squidex.Domain.Apps.Core.EnrichContent
{ {
Guard.NotNull(field, nameof(field)); 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(); 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(); return JValue.CreateNull();
} }
public JToken Visit(JsonFieldProperties properties) public JToken Visit(IField<JsonFieldProperties> field)
{ {
return JValue.CreateNull(); 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(); 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(); 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(); return now.ToString();
} }
if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today)
{ {
return now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds 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()) this.value = value;
{
switch (field)
{
case IField<AssetsFieldProperties> assetsField:
return Visit(assetsField, value, oldReferences);
case IField<ReferencesFieldProperties> referencesField:
return Visit(referencesField, value, oldReferences);
}
}
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); return field.Accept(new ReferencesCleaner(value, oldReferences));
var newIds = oldIds.Except(oldReferences).ToList(); }
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)) if (oldReferences.Contains(field.Properties.SchemaId))
{ {
return new JArray(); return new JArray();
} }
var oldIds = GetIds(value); return CleanIds();
var newIds = oldIds.Except(oldReferences).ToList();
return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value;
} }
private static List<Guid> GetIds(JToken value) private JToken CleanIds()
{ {
try var ids = value.ToGuidSet();
{
return value?.ToObject<List<Guid>>() ?? EmptyIds; var isRemoved = false;
}
catch 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 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) private readonly JToken value;
{
switch (field)
{
case IField<AssetsFieldProperties> assetsField:
return Visit(assetsField, value);
case IField<ReferencesFieldProperties> referencesField: private ReferencesExtractor(JToken value)
return Visit(referencesField, 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; var result = new List<Guid>();
try
{ if (value is JArray items)
result = value?.ToObject<List<Guid>>();
}
catch
{ {
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; var ids = value.ToGuidSet();
try
{ return ids;
result = value?.ToObject<List<Guid>>() ?? Enumerable.Empty<Guid>(); }
}
catch 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds 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); 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); return value;
data[partitionValue.Key] = newValue;
} }
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); return field.Accept(Instance);
} }
public IEdmTypeReference Visit(IArrayField field)
{
return null;
}
public IEdmTypeReference Visit(IField<AssetsFieldProperties> field) public IEdmTypeReference Visit(IField<AssetsFieldProperties> field)
{ {
return CreatePrimitive(EdmPrimitiveTypeKind.String, field); return CreatePrimitive(EdmPrimitiveTypeKind.String, field);

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

@ -7,6 +7,7 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using NJsonSchema; using NJsonSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
@ -21,6 +22,30 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
this.schemaResolver = schemaResolver; 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) public JsonProperty Visit(IField<AssetsFieldProperties> field)
{ {
return CreateProperty(field, jsonProperty => 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(); 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) public override PropertyDescriptor GetOwnProperty(string propertyName)
{ {
EnsurePropertiesInitialized(); 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() 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.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure; using Squidex.Infrastructure;
#pragma warning disable 168 #pragma warning disable SA1028, IDE0004 // Code must not contain trailing whitespace
namespace Squidex.Domain.Apps.Core.ValidateContent namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
public sealed class ContentValidator public sealed class ContentValidator
{ {
private static readonly ContentFieldData DefaultFieldData = new ContentFieldData();
private static readonly JToken DefaultValue = JValue.CreateNull();
private readonly Schema schema; private readonly Schema schema;
private readonly PartitionResolver partitionResolver; private readonly PartitionResolver partitionResolver;
private readonly ValidationContext context; private readonly ValidationContext context;
@ -39,103 +43,60 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
this.partitionResolver = partitionResolver; this.partitionResolver = partitionResolver;
} }
public Task ValidatePartialAsync(NamedContentData data) private void AddError(IEnumerable<string> path, string message)
{ {
Guard.NotNull(data, nameof(data)); var pathString = path.ToPathString();
var tasks = new List<Task>();
foreach (var fieldData in data)
{
var fieldName = fieldData.Key;
if (!schema.FieldsByName.TryGetValue(fieldData.Key, out var field))
{
errors.AddError("<FIELD> is not a known field.", fieldName);
}
else
{
tasks.Add(ValidateFieldPartialAsync(field, fieldData.Value));
}
}
return Task.WhenAll(tasks); errors.Add(new ValidationError($"{pathString}: {message}", pathString));
} }
private Task ValidateFieldPartialAsync(Field field, ContentFieldData fieldData) public Task ValidatePartialAsync(NamedContentData data)
{ {
var partitioning = field.Partitioning; Guard.NotNull(data, nameof(data));
var partition = partitionResolver(partitioning);
var tasks = new List<Task>();
foreach (var partitionValues in fieldData) var validator = CreateSchemaValidator(true);
{
if (partition.TryGetItem(partitionValues.Key, out var item))
{
tasks.Add(field.ValidateAsync(partitionValues.Value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item)));
}
else
{
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field);
}
}
return Task.WhenAll(tasks); return validator.ValidateAsync(data, context, AddError);
} }
public Task ValidateAsync(NamedContentData data) public Task ValidateAsync(NamedContentData data)
{ {
Guard.NotNull(data, nameof(data)); Guard.NotNull(data, nameof(data));
ValidateUnknownFields(data); var validator = CreateSchemaValidator(false);
var tasks = new List<Task>();
foreach (var field in schema.FieldsByName.Values)
{
var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData());
tasks.Add(ValidateFieldAsync(field, fieldData));
}
return Task.WhenAll(tasks); 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)) fieldsValidators[field.Key] = (!field.Value.RawProperties.IsRequired, CreateFieldValidator(field.Value, isPartial));
{
errors.AddError("<FIELD> is not a known field.", fieldData.Key);
}
} }
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 partitioning = partitionResolver(field.Partitioning);
var partition = partitionResolver(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 _)) fieldsValidators[partition.Key] = (partition.IsOptional, fieldValidator);
{
errors.AddError($"<FIELD> has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field);
}
} }
foreach (var item in partition) var isLanguage = field.Partitioning.Equals(Partitioning.Language);
{
var value = fieldData.GetOrCreate(item.Key, k => JValue.CreateNull());
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 sealed class JsonValueConverter : IFieldVisitor<object>
{ {
public JToken Value { get; } private readonly JToken value;
private JsonValueConverter(JToken value) private JsonValueConverter(JToken value)
{ {
Value = value; this.value = value;
} }
public static object ConvertValue(IField field, JToken json) public static object ConvertValue(IField field, JToken json)
@ -28,21 +28,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return field.Accept(new JsonValueConverter(json)); return field.Accept(new JsonValueConverter(json));
} }
public object Visit(IArrayField field)
{
return value.ToObject<List<JObject>>();
}
public object Visit(IField<AssetsFieldProperties> field) public object Visit(IField<AssetsFieldProperties> field)
{ {
return Value.ToObject<List<Guid>>(); return value.ToObject<List<Guid>>();
} }
public object Visit(IField<BooleanFieldProperties> field) public object Visit(IField<BooleanFieldProperties> field)
{ {
return (bool?)Value; return (bool?)value;
} }
public object Visit(IField<DateTimeFieldProperties> field) 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) if (!parseResult.Success)
{ {
@ -57,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public object Visit(IField<GeolocationFieldProperties> field) public object Visit(IField<GeolocationFieldProperties> field)
{ {
var geolocation = (JObject)Value; var geolocation = (JObject)value;
foreach (var property in geolocation.Properties()) 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."); throw new InvalidCastException("Longitude must be between -180 and 180.");
} }
return Value; return value;
} }
public object Visit(IField<JsonFieldProperties> field) public object Visit(IField<JsonFieldProperties> field)
{ {
return Value; return value;
} }
public object Visit(IField<NumberFieldProperties> field) public object Visit(IField<NumberFieldProperties> field)
{ {
return (double?)Value; return (double?)value;
} }
public object Visit(IField<ReferencesFieldProperties> field) public object Visit(IField<ReferencesFieldProperties> field)
{ {
return Value.ToObject<List<Guid>>(); return value.ToObject<List<Guid>>();
} }
public object Visit(IField<StringFieldProperties> field) public object Visit(IField<StringFieldProperties> field)
{ {
return Value.ToString(); return value.ToString();
} }
public object Visit(IField<TagsFieldProperties> field) 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; 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>, Guid, Task<IReadOnlyList<Guid>>> checkContent;
private readonly Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset; 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 bool IsOptional { get; }
public ValidationContext( public ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent, Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent,
Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset) Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset)
: this(checkContent, checkAsset, false) : this(checkContent, checkAsset, ImmutableQueue<string>.Empty, false)
{ {
} }
private ValidationContext( private ValidationContext(
Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent, Func<IEnumerable<Guid>, Guid, Task<IReadOnlyList<Guid>>> checkContent,
Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset, Func<IEnumerable<Guid>, Task<IReadOnlyList<IAssetInfo>>> checkAsset,
ImmutableQueue<string> propertyPath,
bool isOptional) bool isOptional)
{ {
Guard.NotNull(checkAsset, nameof(checkAsset)); Guard.NotNull(checkAsset, nameof(checkAsset));
Guard.NotNull(checkContent, nameof(checkAsset)); Guard.NotNull(checkContent, nameof(checkAsset));
this.propertyPath = propertyPath;
this.checkContent = checkContent; this.checkContent = checkContent;
this.checkAsset = checkAsset; this.checkAsset = checkAsset;
@ -42,7 +52,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public ValidationContext Optional(bool isOptional) 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) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -24,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.allowedValues = allowedValues; 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) if (value == null)
{ {
@ -35,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (!allowedValues.Contains(typedValue)) if (!allowedValues.Contains(typedValue))
{ {
addError("<FIELD> is not an allowed value."); addError(context.Path, "Not an allowed value.");
} }
return TaskHelper.Done; 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; 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 assets = await context.GetAssetInfosAsync(assetIds);
var i = 0; var index = 0;
foreach (var assetId in assetIds) 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) var asset = assets.FirstOrDefault(x => x.AssetId == assetId);
{
addError($"<FIELD> has invalid asset #{i}: {message}");
}
if (asset == null) if (asset == null)
{ {
Error($"Id '{assetId}' not found."); addError(path, $"Id '{assetId}' not found.");
continue; continue;
} }
if (properties.MinSize.HasValue && asset.FileSize < properties.MinSize) 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) 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 && if (properties.AllowedExtensions != null &&
properties.AllowedExtensions.Count > 0 && properties.AllowedExtensions.Count > 0 &&
!properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase))) !properties.AllowedExtensions.Any(x => asset.FileName.EndsWith("." + x, StringComparison.OrdinalIgnoreCase)))
{ {
Error("Invalid file extension."); addError(path, "Invalid file extension.");
} }
if (!asset.IsImage) if (!asset.IsImage)
{ {
if (properties.MustBeImage) if (properties.MustBeImage)
{ {
Error("Not an image."); addError(path, "Not an image.");
} }
continue; continue;
@ -84,22 +81,22 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (properties.MinWidth.HasValue && w < properties.MinWidth) 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) 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) 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) 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) 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) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public sealed class CollectionItemValidator<T> : IValidator public sealed class CollectionItemValidator : IValidator
{ {
private readonly IValidator[] itemValidators; private readonly IValidator[] itemValidators;
@ -24,23 +24,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.itemValidators = itemValidators; 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; var index = 1;
foreach (var item in items) foreach (var item in items)
{ {
var innerContext = context.Nested($"[{index}]");
foreach (var itemValidator in itemValidators) 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++; 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public sealed class CollectionValidator<T> : IValidator public sealed class CollectionValidator : IValidator
{ {
private readonly bool isRequired; private readonly bool isRequired;
private readonly int? minItems; private readonly int? minItems;
@ -25,13 +24,13 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.maxItems = maxItems; 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) if (isRequired && !context.IsOptional)
{ {
addError("<FIELD> is required."); addError(context.Path, "Field is required.");
} }
return TaskHelper.Done; return TaskHelper.Done;
@ -39,12 +38,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (minItems.HasValue && items.Count < minItems.Value) 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) 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; 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public delegate void AddError(IEnumerable<string> path, string message);
public interface IValidator 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); 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) if (value is string stringValue)
{ {
@ -37,17 +37,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
if (string.IsNullOrWhiteSpace(errorMessage)) if (string.IsNullOrWhiteSpace(errorMessage))
{ {
addError("<FIELD> is not valid."); addError(context.Path, "Not valid.");
} }
else else
{ {
addError(errorMessage); addError(context.Path, errorMessage);
} }
} }
} }
catch 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; 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) if (value == null)
{ {
@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
if (min.HasValue && typedValue.CompareTo(min.Value) < 0) 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) 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; 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; 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) if (value is ICollection<Guid> contentIds)
{ {
@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
foreach (var invalidId in invalidIds) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
@ -20,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
this.validateEmptyStrings = validateEmptyStrings; 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))) 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))) if (valueAsString == null || (validateEmptyStrings && string.IsNullOrWhiteSpace(valueAsString)))
{ {
addError("<FIELD> is required."); addError(context.Path, "Field is required.");
} }
return TaskHelper.Done; 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
@ -13,11 +12,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{ {
public class RequiredValidator : IValidator 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) if (value == null && !context.IsOptional)
{ {
addError("<FIELD> is required."); addError(context.Path, "Field is required.");
} }
return TaskHelper.Done; 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; 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 (value is string stringValue && !string.IsNullOrEmpty(stringValue))
{ {
if (minLength.HasValue && stringValue.Length < minLength.Value) 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) 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Domain.Apps.Core.ValidateContent.Validators;
@ -15,7 +16,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.ValidateContent 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(); private static readonly ValidatorsFactory Instance = new ValidatorsFactory();
@ -27,118 +28,135 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
Guard.NotNull(field, nameof(field)); 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(); 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(); 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(); 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(); 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(); 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(); 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) 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) 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) 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 schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id);
var idData = value.Data.ToMongoModel(schema.SchemaDef); 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 var content = SimpleMapper.Map(value, new MongoContentEntity
{ {
DataByIds = idData, DataByIds = idData,
DataDraftByIds = value.DataDraft?.ToMongoModel(schema.SchemaDef), DataDraftByIds = idDraftData,
IsDeleted = value.IsDeleted, IsDeleted = value.IsDeleted,
IndexedAppId = value.AppId.Id, IndexedAppId = value.AppId.Id,
IndexedSchemaId = value.SchemaId.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) 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> var events = new List<AppEvent>
{ {
@ -308,7 +308,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
if (@event.AppId == null) 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)); 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)) 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 => var publish = new Func<ICommand, Task>(command =>
{ {
@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); await publish(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name); var schemaId = NamedId.Of(command.SchemaId, command.Name);
await publish(new ConfigureScripts await publish(new ConfigureScripts
{ {
@ -222,7 +222,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); await publish(command);
var schemaId = new NamedId<Guid>(command.SchemaId, command.Name); var schemaId = NamedId.Of(command.SchemaId, command.Name);
await publish(new ConfigureScripts 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)) 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 => var publish = new Func<ICommand, Task>(command =>
{ {
@ -233,7 +233,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); 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) private async Task<NamedId<Guid>> CreateProjectsSchemaAsync(Func<ICommand, Task> publish)
@ -324,7 +324,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); 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) private async Task<NamedId<Guid>> CreateExperienceSchemaAsync(Func<ICommand, Task> publish)
@ -404,7 +404,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); 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) private async Task<NamedId<Guid>> CreateEducationSchemaAsync(Func<ICommand, Task> publish)
@ -484,7 +484,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); 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) private async Task<NamedId<Guid>> CreatePublicationsSchemaAsync(Func<ICommand, Task> publish)
@ -552,7 +552,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); 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) private async Task<NamedId<Guid>> CreateSkillsSchemaAsync(Func<ICommand, Task> publish)
@ -597,7 +597,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
await publish(command); 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 = 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) if (result.DataDraft != null)
{ {
result.DataDraft = result.DataDraft.Convert(schema.SchemaDef, converters); result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
} }
yield return result; yield return result;
@ -172,11 +172,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!context.IsFrontendClient) if (!context.IsFrontendClient)
{ {
yield return FieldConverters.ExcludeHidden(); yield return FieldConverters.ExcludeHidden();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeHidden());
} }
if (checkType) if (checkType)
{ {
yield return FieldConverters.ExcludeChangedTypes(); yield return FieldConverters.ExcludeChangedTypes();
yield return FieldConverters.ForNestedName2Name(ValueConverters.ExcludeChangedTypes());
} }
yield return FieldConverters.ResolveInvariant(context.App.LanguagesConfig); 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.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL 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 static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery; private readonly IContentQueryService contentQuery;
private readonly ICommandBus commandBus;
private readonly IGraphQLUrlGenerator urlGenerator; private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
@ -28,20 +26,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public CachingGraphQLService(IMemoryCache cache, public CachingGraphQLService(IMemoryCache cache,
IAppProvider appProvider, IAppProvider appProvider,
IAssetRepository assetRepository, IAssetRepository assetRepository,
ICommandBus commandBus,
IContentQueryService contentQuery, IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator) IGraphQLUrlGenerator urlGenerator)
: base(cache) : base(cache)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(contentQuery, nameof(contentQuery)); Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(urlGenerator, nameof(urlGenerator)); Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider; this.appProvider = appProvider;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.commandBus = commandBus;
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator; this.urlGenerator = urlGenerator;
} }
@ -58,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var modelContext = await GetModelAsync(context.App); 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); 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 Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class GraphQLExecutionContext : QueryExecutionContext public sealed class GraphQLExecutionContext : QueryExecutionContext
{ {
public ICommandBus CommandBus { get; }
public IGraphQLUrlGenerator UrlGenerator { get; } public IGraphQLUrlGenerator UrlGenerator { get; }
public GraphQLExecutionContext(QueryContext context, public GraphQLExecutionContext(QueryContext context,
IAssetRepository assetRepository, IAssetRepository assetRepository,
ICommandBus commandBus,
IContentQueryService contentQuery, IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator) IGraphQLUrlGenerator urlGenerator)
: base(context, assetRepository, contentQuery) : base(context, assetRepository, contentQuery)
{ {
CommandBus = commandBus;
UrlGenerator = urlGenerator; UrlGenerator = urlGenerator;
} }

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

@ -12,12 +12,13 @@ using System.Threading.Tasks;
using GraphQL; using GraphQL;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; 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.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using GraphQLSchema = GraphQL.Types.Schema; using GraphQLSchema = GraphQL.Types.Schema;
@ -28,13 +29,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class GraphQLModel : IGraphModel public sealed class GraphQLModel : IGraphModel
{ {
private readonly QueryGraphTypeVisitor schemaTypes;
private readonly Dictionary<ISchemaEntity, ContentGraphType> contentTypes = new Dictionary<ISchemaEntity, ContentGraphType>(); private readonly Dictionary<ISchemaEntity, ContentGraphType> contentTypes = new Dictionary<ISchemaEntity, ContentGraphType>();
private readonly Dictionary<ISchemaEntity, ContentDataGraphType> contentDataTypes = new Dictionary<ISchemaEntity, ContentDataGraphType>(); private readonly Dictionary<ISchemaEntity, ContentDataGraphType> contentDataTypes = new Dictionary<ISchemaEntity, ContentDataGraphType>();
private readonly Dictionary<Guid, ISchemaEntity> schemasById; private readonly Dictionary<Guid, ISchemaEntity> schemasById;
private readonly PartitionResolver partitionResolver; private readonly PartitionResolver partitionResolver;
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IGraphType assetType; private readonly IGraphType assetType;
private readonly IGraphType assetListType;
private readonly GraphQLSchema graphQLSchema; private readonly GraphQLSchema graphQLSchema;
public bool CanGenerateAssetSourceUrl { get; private set; } public bool CanGenerateAssetSourceUrl { get; private set; }
@ -48,10 +49,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl;
assetType = new AssetGraphType(this); assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType));
schemasById = schemas.ToDictionary(x => x.Id); schemasById = schemas.ToDictionary(x => x.Id);
schemaTypes = new QueryGraphTypeVisitor(GetContentType, new ListGraphType(new NonNullGraphType(assetType)));
graphQLSchema = BuildSchema(this); graphQLSchema = BuildSchema(this);
graphQLSchema.RegisterValueConverter(JsonConverter.Instance);
InitializeContentTypes(); InitializeContentTypes();
} }
@ -60,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var schemas = model.schemasById.Values; 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() private void InitializeContentTypes()
@ -78,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(IGraphType type) 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() public IFieldResolver ResolveAssetUrl()
@ -134,14 +137,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key); 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); return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType));
}
public IGraphType GetInputGraphType(IField field)
{
return field.GetInputGraphType();
} }
public IGraphType GetAssetType() public IGraphType GetAssetType()
@ -158,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return null; return null;
} }
return schema != null ? contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType()) : null; return schema != null ? contentDataTypes.GetOrAddNew(schema) : null;
} }
public IGraphType GetContentType(Guid schemaId) public IGraphType GetContentType(Guid schemaId)
@ -170,6 +168,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return null; return null;
} }
contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType());
return contentTypes.GetOrAdd(schema, s => new ContentGraphType()); return contentTypes.GetOrAdd(schema, s => new ContentGraphType());
} }
@ -177,13 +177,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
var inputs = query.Variables?.ToInputs();
var result = await new DocumentExecuter().ExecuteAsync(options => var result = await new DocumentExecuter().ExecuteAsync(options =>
{ {
options.Inputs = query.Variables?.ToInputs() ?? new Inputs();
options.Query = query.Query;
options.OperationName = query.OperationName; options.OperationName = query.OperationName;
options.Schema = graphQLSchema;
options.UserContext = context; options.UserContext = context;
options.Schema = graphQLSchema;
options.Inputs = inputs;
options.Query = query.Query;
}).ConfigureAwait(false); }).ConfigureAwait(false);
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); 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 namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public class GraphQLQuery public sealed class GraphQLQuery
{ {
public string OperationName { get; set; } 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 GraphQL.Types;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
@ -34,8 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IGraphType GetContentDataType(Guid schemaId); IGraphType GetContentDataType(Guid schemaId);
IGraphType GetInputGraphType(IField field); (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field);
(IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(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 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 Float = new FloatGraphType();
public static readonly IGraphType Status = new EnumerationGraphType<Status>();
public static readonly IGraphType String = new StringGraphType(); public static readonly IGraphType String = new StringGraphType();
public static readonly IGraphType Boolean = new BooleanGraphType(); 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 NoopBoolean = new NoopGraphType(Boolean);
public static readonly IGraphType NoopJson = new NoopGraphType("Json");
public static readonly IGraphType NoopTags = new NoopGraphType("Tags"); 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 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Linq; using System.Linq;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; 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)) 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) if (fieldInfo.ResolveType != null)
{ {
var fieldName = field.RawProperties.Label.WithFallback(field.Name); var fieldType = field.TypeName();
var fieldName = field.DisplayName();
var fieldGraphType = new ObjectGraphType var fieldGraphType = new ObjectGraphType
{ {
Name = $"{schemaType}Data{field.Name.ToPascalCase()}Dto" Name = $"{schemaType}Data{fieldType}Dto"
}; };
var partition = model.ResolvePartition(field.Partitioning); var partition = model.ResolvePartition(field.Partitioning);
foreach (var partitionItem in partition) 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 fieldGraphType.AddField(new FieldType
{ {
Name = partitionItem.Key, Name = partitionItem.Key,
Resolver = fieldInfo.Resolver, Resolver = resolver,
ResolvedType = fieldInfo.ResolveType, ResolvedType = fieldInfo.ResolveType,
Description = field.RawProperties.Hints 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 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 System;
using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents; using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types 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 Func<Guid, IGraphType> schemaResolver;
private readonly IGraphModel model;
private readonly IGraphType assetListType; 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.assetListType = assetListType;
this.schema = schema;
this.schemaResolver = schemaResolver; 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); 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); 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); 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); 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); 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); 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); 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); 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 context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedAssetsAsync(contentIds); return context.GetReferencedAssetsAsync(value);
}); });
return (assetListType, resolver); return (assetListType, resolver);
} }
private ValueTuple<IGraphType, IFieldResolver> ResolveReferences(IField field) private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field)
{ {
var schemaId = ((ReferencesFieldProperties)field.RawProperties).SchemaId; var schemaId = ((ReferencesFieldProperties)field.RawProperties).SchemaId;
@ -99,12 +116,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return (null, null); return (null, null);
} }
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => var resolver = new ValueResolver((value, c) =>
{ {
var context = (GraphQLExecutionContext)c.UserContext; 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)); 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.Language.AST;
using GraphQL.Types; 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 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using GraphQL.Language.AST; using GraphQL.Language.AST;
using GraphQL.Types; using GraphQL.Types;
@ -18,6 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
Name = name; Name = name;
} }
public NoopGraphType(IGraphType type)
: this(type.Name)
{
Description = type.Description;
}
public override object Serialize(object value) public override object Serialize(object value)
{ {
return value; return value;
@ -30,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
public override object ParseLiteral(IValue value) 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 sealed class AddField : SchemaCommand
{ {
public long? ParentFieldId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Partitioning { 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 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 namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
@ -21,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
public bool IsDisabled { get; set; } public bool IsDisabled { get; set; }
public FieldNested Nested { get; set; }
public FieldProperties Properties { 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 class FieldCommand : SchemaCommand
{ {
public long? ParentFieldId { get; set; }
public long FieldId { 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 sealed class ReorderFields : SchemaCommand
{ {
public long? ParentFieldId { get; set; }
public List<long> FieldIds { 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.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -22,7 +21,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
public static IEnumerable<ValidationError> Validate(FieldProperties properties) 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) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core; 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))); 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; var index = 0;
foreach (var field in command.Fields) foreach (var field in command.Fields)
{ {
index++;
var prefix = $"Fields.{index}"; var prefix = $"Fields.{index}";
if (!field.Partitioning.IsValidPartitioning()) 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)}")); 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)); 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 => Validate.It(() => "Cannot reorder schema fields.", error =>
{ {
if (command.FieldIds == null) 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))); 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) public static void CanPublish(Schema schema, PublishSchema command)
{ {
Guard.NotNull(command, nameof(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 => 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()) if (!command.Name.IsPropertyName())
{ {
error(new ValidationError("Name must be a valid property name.", nameof(command.Name))); 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))); 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)));
}
} }
else
if (schema.FieldsByName.ContainsKey(command.Name))
{ {
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)); 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 => Validate.It(() => "Cannot update field.", error =>
{ {
if (command.Properties == null) if (command.Properties == null)
{ {
error(new ValidationError("Properties is required.", nameof(command.Properties))); error(new ValidationError("Properties is required.", nameof(command.Properties)));
} }
else
var propertyErrors = FieldPropertiesValidator.Validate(command.Properties);
foreach (var propertyError in propertyErrors)
{ {
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)); Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId); var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked) if (field.IsLocked)
{ {
throw new DomainException("Schema field is locked."); 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)); 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)); 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)); 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) 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) if (!field.IsDisabled)
{ {
@ -138,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
var field = GetFieldOrThrow(schema, command.FieldId); var field = GetFieldOrThrow(schema, command.FieldId, command.ParentFieldId);
if (field.IsLocked) 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)) if (!schema.FieldsById.TryGetValue(fieldId, out var field))
{ {
throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema)); throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema));

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

Loading…
Cancel
Save