Browse Source

Merge pull request #343 from Squidex/v3_schema_sync

V3 schema sync
pull/345/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
9811805add
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      extensions/Squidex.Extensions/Squidex.Extensions.csproj
  2. 5
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  3. 7
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
  4. 48
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
  5. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
  6. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs
  7. 8
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs
  8. 41
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  9. 76
      src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  10. 55
      src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs
  11. 22
      src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs
  12. 14
      src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs
  13. 215
      src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  14. 38
      src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs
  15. 2
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  16. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  17. 2
      src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs
  18. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs
  19. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs
  20. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs
  21. 4
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs
  22. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs
  23. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs
  24. 41
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs
  25. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs
  26. 2
      src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs
  27. 25
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  28. 23
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs
  29. 48
      src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs
  30. 2
      src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs
  31. 2
      src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
  32. 10
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  33. 14
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  34. 4
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  35. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  36. 1
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  37. 6
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  38. 4
      src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs
  39. 4
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/AddField.cs
  40. 12
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs
  41. 16
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs
  42. 4
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldCommand.cs
  43. 7
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs
  44. 4
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/ReorderFields.cs
  45. 16
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs
  46. 116
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs
  47. 7
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs
  48. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs
  49. 2
      src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs
  50. 121
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  51. 21
      src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs
  52. 27
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs
  53. 142
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  54. 12
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs
  55. 165
      src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs
  56. 2
      src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  57. 4
      src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs
  58. 16
      src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs
  59. 15
      src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs
  60. 5
      src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs
  61. 15
      src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs
  62. 2
      src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
  63. 4
      src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
  64. 4
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  65. 22
      src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs
  66. 2
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  67. 2
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs
  68. 20
      src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.cs
  69. 61
      src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs
  70. 38
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs
  71. 17
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs
  72. 29
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs
  73. 98
      src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertDto.cs
  74. 4
      src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs
  75. 2
      src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs
  76. 2
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  77. 36
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  78. 3
      src/Squidex/Config/Domain/EntitiesServices.cs
  79. 4
      src/Squidex/Config/Domain/SerializationServices.cs
  80. 2
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
  81. 2
      src/Squidex/Pipeline/UrlGenerator.cs
  82. 12
      src/Squidex/Squidex.csproj
  83. 2
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html
  84. 4
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  85. 8
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  86. 2
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts
  87. 31
      src/Squidex/app/shared/services/schemas.service.spec.ts
  88. 41
      src/Squidex/app/shared/services/schemas.service.ts
  89. 2
      src/Squidex/app/shared/state/contents.forms.spec.ts
  90. 12
      src/Squidex/app/shared/state/schemas.forms.ts
  91. 32
      src/Squidex/app/shared/state/schemas.state.spec.ts
  92. 13
      src/Squidex/app/shared/state/schemas.state.ts
  93. 67
      tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs
  94. 51
      tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs
  95. 51
      tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs
  96. 526
      tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs
  97. 4
      tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  98. 4
      tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs
  99. 69
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs
  100. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs

2
extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="6.4.1" />
<PackageReference Include="Elasticsearch.Net" Version="6.4.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />

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

@ -31,6 +31,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return fields.ByName; }
}
public FieldCollection<NestedField> FieldCollection
{
get { return fields; }
}
public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null)
: base(id, name, partitioning, properties)
{

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

@ -5,12 +5,19 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System.Collections.Generic;
using NamedIdStatic = Squidex.Infrastructure.NamedId;
namespace Squidex.Domain.Apps.Core.Schemas
{
public static class FieldExtensions
{
public static NamedId<long> NamedId(this IField field)
{
return NamedIdStatic.Of(field.Id, field.Name);
}
public static Schema ReorderFields(this Schema schema, List<long> ids, long? parentId = null)
{
if (parentId != null)

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

@ -12,58 +12,26 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class FieldRegistry
public static class FieldRegistry
{
private readonly TypeNameRegistry typeNameRegistry;
private readonly HashSet<Type> supportedFields = new HashSet<Type>();
public FieldRegistry(TypeNameRegistry typeNameRegistry)
public static void Setup(TypeNameRegistry typeNameRegistry)
{
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
this.typeNameRegistry = typeNameRegistry;
var types = typeof(FieldRegistry).Assembly.GetTypes().Where(x => x.BaseType == typeof(FieldProperties));
var supportedFields = new HashSet<Type>();
foreach (var type in types)
{
RegisterField(type);
if (supportedFields.Add(type))
{
typeNameRegistry.Map(type);
}
}
typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "References");
typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "DateTime");
}
private void RegisterField(Type type)
{
if (supportedFields.Add(type))
{
typeNameRegistry.Map(type);
}
}
public RootField CreateRootField(long id, string name, Partitioning partitioning, FieldProperties properties, IFieldSettings settings = null)
{
CheckProperties(properties);
return properties.CreateRootField(id, name, partitioning, settings);
}
public NestedField CreateNestedField(long id, string name, FieldProperties properties, IFieldSettings settings = null)
{
CheckProperties(properties);
return properties.CreateNestedField(id, name, settings);
}
private void CheckProperties(FieldProperties properties)
{
Guard.NotNull(properties, nameof(properties));
if (!supportedFields.Contains(properties.GetType()))
{
throw new InvalidOperationException($"The field property '{properties.GetType()}' is not supported.");
}
}
}
}

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

@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
return new NestedField<TagsFieldProperties>(id, name, properties, settings);
}
public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func<ArrayField, ArrayField> handler, ArrayFieldProperties properties = null, IFieldSettings settings = null)
public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func<ArrayField, ArrayField> handler = null, ArrayFieldProperties properties = null, IFieldSettings settings = null)
{
var field = Array(id, name, partitioning, properties, settings);

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

@ -16,5 +16,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
IReadOnlyDictionary<long, NestedField> FieldsById { get; }
IReadOnlyDictionary<string, NestedField> FieldsByName { get; }
FieldCollection<NestedField> FieldCollection { get; }
}
}

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

@ -20,17 +20,15 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
[JsonProperty]
public bool IsHidden { get; set; }
[JsonProperty]
public bool IsLocked { get; set; }
[JsonProperty]
public bool IsDisabled { get; set; }
[JsonProperty]
public FieldProperties Properties { get; set; }
public bool IsLocked
{
get { return false; }
}
public NestedField ToNestedField()
{
return Properties.CreateNestedField(Id, Name, this);

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

@ -6,8 +6,11 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Schemas.Json
{
@ -16,24 +19,34 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public string Category { get; set; }
[JsonProperty]
public bool IsSingleton { get; set; }
[JsonProperty]
public bool IsPublished { get; set; }
[JsonProperty]
public SchemaProperties Properties { get; set; }
[JsonProperty]
public SchemaScripts Scripts { get; set; }
[JsonProperty]
public JsonFieldModel[] Fields { get; set; }
[JsonProperty]
public Dictionary<string, string> PreviewUrls { get; set; }
public JsonSchemaModel()
{
}
public JsonSchemaModel(Schema schema)
{
Name = schema.Name;
Properties = schema.Properties;
SimpleMapper.Map(schema, this);
Fields =
schema.Fields.ToArray(x =>
@ -49,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
Properties = x.RawProperties
});
IsPublished = schema.IsPublished;
PreviewUrls = schema.PreviewUrls.ToDictionary(x => x.Key, x => x.Value);
}
private static JsonNestedFieldModel[] CreateChildren(IField field)
@ -62,6 +75,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
Id = x.Id,
Name = x.Name,
IsHidden = x.IsHidden,
IsLocked = x.IsLocked,
IsDisabled = x.IsDisabled,
Properties = x.RawProperties
});
@ -74,7 +88,24 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
{
var fields = Fields.ToArray(f => f.ToField()) ?? Array.Empty<RootField>();
return new Schema(Name, fields, Properties, IsPublished);
var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton);
if (!string.IsNullOrWhiteSpace(Category))
{
schema = schema.ChangeCategory(Category);
}
if (Scripts != null)
{
schema = schema.ConfigureScripts(Scripts);
}
if (PreviewUrls?.Count > 0)
{
schema = schema.ConfigurePreviewUrls(PreviewUrls);
}
return schema;
}
}
}

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

@ -14,8 +14,13 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class Schema : Cloneable<Schema>
{
private static readonly Dictionary<string, string> EmptyPreviewUrls = new Dictionary<string, string>();
private readonly string name;
private readonly bool isSingleton;
private string category;
private FieldCollection<RootField> fields = FieldCollection<RootField>.Empty;
private IReadOnlyDictionary<string, string> previewUrls = EmptyPreviewUrls;
private SchemaScripts scripts = new SchemaScripts();
private SchemaProperties properties;
private bool isPublished;
@ -24,11 +29,21 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return name; }
}
public string Category
{
get { return category; }
}
public bool IsPublished
{
get { return isPublished; }
}
public bool IsSingleton
{
get { return isSingleton; }
}
public IReadOnlyList<RootField> Fields
{
get { return fields.Ordered; }
@ -44,12 +59,27 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return fields.ByName; }
}
public IReadOnlyDictionary<string, string> PreviewUrls
{
get { return previewUrls; }
}
public FieldCollection<RootField> FieldCollection
{
get { return fields; }
}
public SchemaScripts Scripts
{
get { return scripts; }
}
public SchemaProperties Properties
{
get { return properties; }
}
public Schema(string name, SchemaProperties properties = null)
public Schema(string name, SchemaProperties properties = null, bool isSingleton = false)
{
Guard.NotNullOrEmpty(name, nameof(name));
@ -57,10 +87,12 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.properties = properties ?? new SchemaProperties();
this.properties.Freeze();
this.isSingleton = isSingleton;
}
public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished)
: this(name, properties)
public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished, bool isSingleton = false)
: this(name, properties, isSingleton)
{
Guard.NotNull(fields, nameof(fields));
@ -81,6 +113,16 @@ namespace Squidex.Domain.Apps.Core.Schemas
});
}
[Pure]
public Schema ConfigureScripts(SchemaScripts newScripts)
{
return Clone(clone =>
{
clone.scripts = newScripts ?? new SchemaScripts();
clone.scripts.Freeze();
});
}
[Pure]
public Schema Publish()
{
@ -99,31 +141,49 @@ namespace Squidex.Domain.Apps.Core.Schemas
});
}
[Pure]
public Schema ChangeCategory(string category)
{
return Clone(clone =>
{
clone.category = category;
});
}
[Pure]
public Schema ConfigurePreviewUrls(IReadOnlyDictionary<string, string> previewUrls)
{
return Clone(clone =>
{
clone.previewUrls = previewUrls ?? EmptyPreviewUrls;
});
}
[Pure]
public Schema DeleteField(long fieldId)
{
return Updatefields(f => f.Remove(fieldId));
return UpdateFields(f => f.Remove(fieldId));
}
[Pure]
public Schema ReorderFields(List<long> ids)
{
return Updatefields(f => f.Reorder(ids));
return UpdateFields(f => f.Reorder(ids));
}
[Pure]
public Schema AddField(RootField field)
{
return Updatefields(f => f.Add(field));
return UpdateFields(f => f.Add(field));
}
[Pure]
public Schema UpdateField(long fieldId, Func<RootField, RootField> updater)
{
return Updatefields(f => f.Update(fieldId, updater));
return UpdateFields(f => f.Update(fieldId, updater));
}
private Schema Updatefields(Func<FieldCollection<RootField>, FieldCollection<RootField>> updater)
private Schema UpdateFields(Func<FieldCollection<RootField>, FieldCollection<RootField>> updater)
{
var newFields = updater(fields);

55
src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs

@ -0,0 +1,55 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Schemas
{
public static class SchemaExtensions
{
public static long MaxId(this Schema schema)
{
var id = 0L;
foreach (var field in schema.Fields)
{
if (field is IArrayField arrayField)
{
foreach (var nestedField in arrayField.Fields)
{
id = Math.Max(id, nestedField.Id);
}
}
id = Math.Max(id, field.Id);
}
return id;
}
public static string TypeName(this IField field)
{
return field.Name.ToPascalCase();
}
public static string DisplayName(this IField field)
{
return field.RawProperties.Label.WithFallback(field.TypeName());
}
public static string TypeName(this Schema schema)
{
return schema.Name.ToPascalCase();
}
public static string DisplayName(this Schema schema)
{
return schema.Properties.Label.WithFallback(schema.TypeName());
}
}
}

22
src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class SchemaScripts : Freezable
{
public string Change { get; set; }
public string Create { get; set; }
public string Update { get; set; }
public string Delete { get; set; }
public string Query { get; set; }
}
}

14
src/Squidex.Domain.Apps.Entities/Apps/Templates/Scripts.cs → src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizationOptions.cs

@ -5,18 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Apps.Templates
namespace Squidex.Domain.Apps.Core.EventSynchronization
{
public static class Scripts
public sealed class SchemaSynchronizationOptions
{
public const string Slug =
@"var data = ctx.data;
public bool NoFieldDeletion { get; set; }
if (data.title && data.title.iv) {
data.slug = { iv: slugify(data.title.iv) };
replace(data);
}
";
public bool NoFieldRecreation { get; set; }
}
}

215
src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs

@ -0,0 +1,215 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.EventSynchronization
{
public static class SchemaSynchronizer
{
public static IEnumerable<IEvent> Synchronize(this Schema source, Schema target, IJsonSerializer serializer, Func<long> idGenerator, SchemaSynchronizationOptions options = null)
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(idGenerator, nameof(idGenerator));
if (target == null)
{
yield return new SchemaDeleted();
}
else
{
options = options ?? new SchemaSynchronizationOptions();
SchemaEvent E(SchemaEvent @event)
{
return @event;
}
if (!source.Properties.EqualsJson(target.Properties, serializer))
{
yield return E(new SchemaUpdated { Properties = target.Properties });
}
if (!source.Category.StringEquals(target.Category))
{
yield return E(new SchemaCategoryChanged { Name = target.Category });
}
if (!source.Scripts.EqualsJson(target.Scripts, serializer))
{
yield return E(new SchemaScriptsConfigured { Scripts = target.Scripts });
}
if (!source.PreviewUrls.EqualsDictionary(target.PreviewUrls))
{
yield return E(new SchemaPreviewUrlsConfigured { PreviewUrls = target.PreviewUrls.ToDictionary(x => x.Key, x => x.Value) });
}
if (source.IsPublished != target.IsPublished)
{
yield return target.IsPublished ?
E(new SchemaPublished()) :
E(new SchemaUnpublished());
}
var events = SyncFields(source.FieldCollection, target.FieldCollection, serializer, idGenerator, null, options);
foreach (var @event in events)
{
yield return E(@event);
}
}
}
private static IEnumerable<SchemaEvent> SyncFields<T>(
FieldCollection<T> source,
FieldCollection<T> target,
IJsonSerializer serializer,
Func<long> idGenerator,
NamedId<long> parentId, SchemaSynchronizationOptions options) where T : class, IField
{
FieldEvent E(FieldEvent @event)
{
@event.ParentFieldId = parentId;
return @event;
}
var sourceIds = new List<NamedId<long>>(source.Ordered.Select(x => x.NamedId()));
var sourceNames = sourceIds.Select(x => x.Name).ToList();
if (!options.NoFieldDeletion)
{
foreach (var sourceField in source.Ordered)
{
if (!target.ByName.TryGetValue(sourceField.Name, out var targetField))
{
var id = sourceField.NamedId();
sourceIds.Remove(id);
sourceNames.Remove(id.Name);
yield return E(new FieldDeleted { FieldId = id });
}
}
}
foreach (var targetField in target.Ordered)
{
NamedId<long> id = null;
var canCreateField = true;
if (source.ByName.TryGetValue(targetField.Name, out var sourceField))
{
canCreateField = false;
id = sourceField.NamedId();
if (CanUpdate(sourceField, targetField))
{
if (!sourceField.RawProperties.EqualsJson(targetField.RawProperties, serializer))
{
yield return E(new FieldUpdated { FieldId = id, Properties = targetField.RawProperties });
}
}
else if (!sourceField.IsLocked && !options.NoFieldRecreation)
{
canCreateField = true;
sourceIds.Remove(id);
sourceNames.Remove(id.Name);
yield return E(new FieldDeleted { FieldId = id });
}
}
if (canCreateField)
{
var partitioning = (string)null;
if (targetField is IRootField rootField)
{
partitioning = rootField.Partitioning.Key;
}
id = NamedId.Of(idGenerator(), targetField.Name);
yield return new FieldAdded
{
Name = targetField.Name,
ParentFieldId = parentId,
Partitioning = partitioning,
Properties = targetField.RawProperties,
FieldId = id
};
sourceIds.Add(id);
sourceNames.Add(id.Name);
}
if (id != null && (sourceField == null || CanUpdate(sourceField, targetField)))
{
if (!targetField.IsLocked.BoolEquals(sourceField?.IsLocked))
{
yield return E(new FieldLocked { FieldId = id });
}
if (!targetField.IsHidden.BoolEquals(sourceField?.IsHidden))
{
yield return targetField.IsHidden ?
E(new FieldHidden { FieldId = id }) :
E(new FieldShown { FieldId = id });
}
if (!targetField.IsDisabled.BoolEquals(sourceField?.IsDisabled))
{
yield return targetField.IsDisabled ?
E(new FieldDisabled { FieldId = id }) :
E(new FieldEnabled { FieldId = id });
}
if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField)
{
var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, id, options);
foreach (var @event in events)
{
yield return @event;
}
}
}
}
if (sourceNames.Count > 1)
{
var targetNames = target.Ordered.Select(x => x.Name);
if (sourceNames.Intersect(targetNames).Count() == target.Ordered.Count && !sourceNames.SequenceEqual(targetNames))
{
yield return new SchemaFieldsReordered { FieldIds = sourceIds.Select(x => x.Id).ToList(), ParentFieldId = parentId };
}
}
}
private static bool CanUpdate(IField source, IField target)
{
return !source.IsLocked && source.Name == target.Name && source.RawProperties.TypeEquals(target.RawProperties);
}
}
}

38
src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.Json;
namespace Squidex.Domain.Apps.Core.EventSynchronization
{
public static class SyncHelpers
{
public static bool BoolEquals(this bool lhs, bool? rhs)
{
return lhs == (rhs ?? false);
}
public static bool StringEquals(this string lhs, string rhs)
{
return string.Equals(lhs ?? string.Empty, rhs ?? string.Empty, StringComparison.Ordinal);
}
public static bool TypeEquals(this object lhs, object rhs)
{
return lhs.GetType() == rhs.GetType();
}
public static bool EqualsJson<T>(this T lhs, T rhs, IJsonSerializer serializer)
{
var lhsJson = serializer.Serialize(lhs);
var rhsJson = serializer.Serialize(rhs);
return string.Equals(lhsJson, rhsJson);
}
}
}

2
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1427" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="NJsonSchema" Version="9.13.11" />
<PackageReference Include="NJsonSchema" Version="9.13.17" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" />

2
src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -17,7 +17,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="MongoDB.Driver" Version="2.7.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

2
src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs

@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var schemaNames = new List<string>();
schemaNames.Add(Permission.Any);
schemaNames.AddRange(schemas.Select(x => x.Name));
schemaNames.AddRange(schemas.Select(x => x.SchemaDef.Name));
return schemaNames;
}

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/AssetFieldBuilder.cs

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class AssetFieldBuilder : FieldBuilder
{
public AssetFieldBuilder(CreateSchemaField field)
public AssetFieldBuilder(UpsertSchemaField field)
: base(field)
{
}

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/BooleanFieldBuilder.cs

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class BooleanFieldBuilder : FieldBuilder
{
public BooleanFieldBuilder(CreateSchemaField field)
public BooleanFieldBuilder(UpsertSchemaField field)
: base(field)
{
}

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/DateTimeFieldBuilder.cs

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class DateTimeFieldBuilder : FieldBuilder
{
public DateTimeFieldBuilder(CreateSchemaField field)
public DateTimeFieldBuilder(UpsertSchemaField field)
: base(field)
{
}

4
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/FieldBuilder.cs

@ -13,14 +13,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public abstract class FieldBuilder
{
private readonly CreateSchemaField field;
private readonly UpsertSchemaField field;
protected T Properties<T>() where T : FieldProperties
{
return field.Properties as T;
}
protected FieldBuilder(CreateSchemaField field)
protected FieldBuilder(UpsertSchemaField field)
{
this.field = field;
}

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/JsonFieldBuilder.cs

@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class JsonFieldBuilder : FieldBuilder
{
public JsonFieldBuilder(CreateSchemaField field)
public JsonFieldBuilder(UpsertSchemaField field)
: base(field)
{
}

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/NumberFieldBuilder.cs

@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class NumberFieldBuilder : FieldBuilder
{
public NumberFieldBuilder(CreateSchemaField field)
public NumberFieldBuilder(UpsertSchemaField field)
: base(field)
{
}

41
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/SchemaBuilder.cs

@ -24,20 +24,39 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
public static SchemaBuilder Create(string name)
{
var schemaName = name.ToKebabCase();
return new SchemaBuilder(new CreateSchema
{
Name = name.ToKebabCase(),
Publish = true,
Properties = new SchemaProperties
{
Label = name
}
});
Name = schemaName
}).Published().WithLabel(name);
}
public SchemaBuilder WithLabel(string label)
{
command.Properties = command.Properties ?? new SchemaProperties();
command.Properties.Label = label;
return this;
}
public SchemaBuilder WithScripts(SchemaScripts scripts)
{
command.Scripts = scripts;
return this;
}
public SchemaBuilder Published()
{
command.IsPublished = true;
return this;
}
public SchemaBuilder Singleton()
{
command.Singleton = true;
command.IsSingleton = true;
return this;
}
@ -105,9 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
return this;
}
private CreateSchemaField AddField<T>(string name) where T : FieldProperties, new()
private UpsertSchemaField AddField<T>(string name) where T : FieldProperties, new()
{
var field = new CreateSchemaField
var field = new UpsertSchemaField
{
Name = name.ToCamelCase(),
Properties = new T
@ -116,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
}
};
command.Fields = command.Fields ?? new List<CreateSchemaField>();
command.Fields = command.Fields ?? new List<UpsertSchemaField>();
command.Fields.Add(field);
return field;

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/StringFieldBuilder.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class StringFieldBuilder : FieldBuilder
{
public StringFieldBuilder(CreateSchemaField field)
public StringFieldBuilder(UpsertSchemaField field)
: base(field)
{
}

2
src/Squidex.Domain.Apps.Entities/Apps/Templates/Builders/TagsFieldBuilder.cs

@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates.Builders
{
public class TagsFieldBuilder : FieldBuilder
{
public TagsFieldBuilder(CreateSchemaField field)
public TagsFieldBuilder(UpsertSchemaField field)
: base(field)
{
}

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

@ -11,7 +11,6 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -106,20 +105,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
.Disabled()
.Label("Slug (Autogenerated)")
.Hints("Autogenerated slug that can be used to identity the post."))
.WithScripts(DefaultScripts.GenerateSlug)
.Build();
await publish(schema);
var schemaId = NamedId.Of(schema.SchemaId, schema.Name);
await publish(new ConfigureScripts
{
SchemaId = schemaId.Id,
ScriptCreate = Scripts.Slug,
ScriptUpdate = Scripts.Slug
});
return schemaId;
return NamedId.Of(schema.SchemaId, schema.Name);
}
private static async Task<NamedId<Guid>> CreatePagesSchemaAsync(Func<ICommand, Task> publish)
@ -140,20 +131,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
.Disabled()
.Label("Slug (Autogenerated)")
.Hints("Autogenerated slug that can be used to identity the page."))
.WithScripts(DefaultScripts.GenerateSlug)
.Build();
await publish(schema);
var schemaId = NamedId.Of(schema.SchemaId, schema.Name);
await publish(new ConfigureScripts
{
SchemaId = schemaId.Id,
ScriptCreate = Scripts.Slug,
ScriptUpdate = Scripts.Slug
});
return schemaId;
return NamedId.Of(schema.SchemaId, schema.Name);
}
}
}

23
src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateIdentityCommandMiddleware.cs

@ -9,7 +9,6 @@ using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates.Builders;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -18,18 +17,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
public sealed class CreateIdentityCommandMiddleware : ICommandMiddleware
{
private const string TemplateName = "Identity";
private const string NormalizeScript = @"
var data = ctx.data;
if (data.userName && data.userName.iv) {
data.normalizedUserName = { iv: data.userName.iv.toUpperCase() };
}
if (data.email && data.email.iv) {
data.normalizedEmail = { iv: data.email.iv.toUpperCase() };
}
replace(data);";
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
@ -241,18 +228,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
.AddString("Security Stamp", f => f
.Disabled()
.Hints("Internal security stamp"))
.WithScripts(DefaultScripts.GenerateUsername)
.Build();
await publish(schema);
var schemaId = NamedId.Of(schema.SchemaId, schema.Name);
await publish(new ConfigureScripts
{
SchemaId = schemaId.Id,
ScriptCreate = NormalizeScript,
ScriptUpdate = NormalizeScript
});
}
private static Task CreateApiResourcesSchemaAsync(Func<ICommand, Task> publish)

48
src/Squidex.Domain.Apps.Entities/Apps/Templates/DefaultScripts.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Apps.Templates
{
public static class DefaultScripts
{
private const string ScriptToGenerateSlug = @"
var data = ctx.data;
if (data.title && data.title.iv) {
data.slug = { iv: slugify(data.title.iv) };
replace(data);
}";
private const string ScriptToGenerateUsername = @"
var data = ctx.data;
if (data.userName && data.userName.iv) {
data.normalizedUserName = { iv: data.userName.iv.toUpperCase() };
}
if (data.email && data.email.iv) {
data.normalizedEmail = { iv: data.email.iv.toUpperCase() };
}
replace(data);";
public static readonly SchemaScripts GenerateSlug = new SchemaScripts
{
Create = ScriptToGenerateSlug,
Update = ScriptToGenerateSlug
};
public static readonly SchemaScripts GenerateUsername = new SchemaScripts
{
Create = ScriptToGenerateUsername,
Update = ScriptToGenerateUsername
};
}
}

2
src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs

@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{
var newGuid = GenerateNewGuid(namedId.Id);
strings[value] = result = new NamedId<Guid>(newGuid, namedId.Name).ToString();
strings[value] = result = NamedId.Of(newGuid, namedId.Name).ToString();
return true;
}

2
src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs

@ -301,7 +301,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
if (@event.Payload is AppEvent appEvent && !string.IsNullOrWhiteSpace(CurrentJob.NewAppName))
{
appEvent.AppId = new NamedId<Guid>(appEvent.AppId.Id, CurrentJob.NewAppName);
appEvent.AppId = NamedId.Of(appEvent.AppId.Id, CurrentJob.NewAppName);
}
foreach (var handler in handlers)

10
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
GuardContent.CanCreate(ctx.Schema, c);
await ctx.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create", c, c.Data);
await ctx.ExecuteScriptAndTransformAsync(s => s.Create, "Create", c, c.Data);
await ctx.EnrichAsync(c.Data);
if (!c.DoNotValidate)
@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (c.Publish)
{
await ctx.ExecuteScriptAsync(x => x.ScriptChange, "Published", c, c.Data);
await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data);
}
Create(c);
@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
reason = StatusChange.Restored;
}
await ctx.ExecuteScriptAsync(x => x.ScriptChange, reason, c, Snapshot.Data);
await ctx.ExecuteScriptAsync(s => s.Change, reason, c, Snapshot.Data);
ChangeStatus(c, reason);
}
@ -166,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
GuardContent.CanDelete(ctx.Schema, c);
await ctx.ExecuteScriptAsync(x => x.ScriptDelete, "Delete", c, Snapshot.Data);
await ctx.ExecuteScriptAsync(s => s.Delete, "Delete", c, Snapshot.Data);
Delete(c);
});
@ -208,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ctx.ValidateAsync(c.Data);
}
newData = await ctx.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update", c, newData, Snapshot.Data);
newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, "Update", c, newData, Snapshot.Data);
if (isProposal)
{

14
src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.EnrichContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps;
@ -86,20 +87,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message);
}
public Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<ISchemaEntity, string> script, object operation, ContentCommand command, NamedContentData data, NamedContentData oldData = null)
public Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, object operation, ContentCommand command, NamedContentData data, NamedContentData oldData = null)
{
var ctx = CreateScriptContext(operation, command, data, oldData);
var result = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity));
var result = scriptEngine.ExecuteAndTransform(ctx, GetScript(script));
return Task.FromResult(result);
}
public Task ExecuteScriptAsync(Func<ISchemaEntity, string> script, object operation, ContentCommand command, NamedContentData data, NamedContentData oldData = null)
public Task ExecuteScriptAsync(Func<SchemaScripts, string> script, object operation, ContentCommand command, NamedContentData data, NamedContentData oldData = null)
{
var ctx = CreateScriptContext(operation, command, data, oldData);
scriptEngine.Execute(ctx, script(schemaEntity));
scriptEngine.Execute(ctx, GetScript(script));
return TaskHelper.Done;
}
@ -123,5 +124,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode);
}
private string GetScript(Func<SchemaScripts, string> script)
{
return script(schemaEntity.SchemaDef.Scripts);
}
}
}

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

@ -158,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var converters = GenerateConverters(context, checkType).ToArray();
var scriptText = schema.ScriptQuery;
var scriptText = schema.SchemaDef.Scripts.Query;
var isScripting = !string.IsNullOrWhiteSpace(scriptText);
@ -277,7 +277,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private static void CheckPermission(ISchemaEntity schema, ClaimsPrincipal user)
{
var permissions = user.Permissions();
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.Name);
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name);
if (!permissions.Allows(permission))
{

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

@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType));
schemasById = schemas.Where(x => x.IsPublished).ToDictionary(x => x.Id);
schemasById = schemas.Where(x => x.SchemaDef.IsPublished).ToDictionary(x => x.Id);
graphQLSchema = BuildSchema(this);
graphQLSchema.RegisterValueConverter(JsonConverter.Instance);

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

@ -10,6 +10,7 @@ using System.Linq;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;

6
src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
ValidateData(command, e);
});
if (schema.IsSingleton && command.ContentId != schema.Id)
if (schema.SchemaDef.IsSingleton && command.ContentId != schema.Id)
{
throw new DomainException("Singleton content cannot be created.");
}
@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
if (schema.IsSingleton && command.Status != Status.Published)
if (schema.SchemaDef.IsSingleton && command.Status != Status.Published)
{
throw new DomainException("Singleton content archived or unpublished.");
}
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
Guard.NotNull(command, nameof(command));
if (schema.IsSingleton)
if (schema.SchemaDef.IsSingleton)
{
throw new DomainException("Singleton content cannot be deleted.");
}

4
src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs

@ -24,9 +24,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (context.IsCompleted &&
context.Command is CreateSchema createSchema &&
createSchema.Singleton)
createSchema.IsSingleton)
{
var schemaId = new NamedId<Guid>(createSchema.SchemaId, createSchema.Name);
var schemaId = NamedId.Of(createSchema.SchemaId, createSchema.Name);
var data = new NamedContentData();

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

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

12
src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs

@ -5,18 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class ConfigureScripts : SchemaCommand
{
public string ScriptQuery { get; set; }
public string ScriptCreate { get; set; }
public string ScriptUpdate { get; set; }
public string ScriptDelete { get; set; }
public string ScriptChange { get; set; }
public SchemaScripts Scripts { get; set; }
}
}

16
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchema.cs

@ -8,27 +8,25 @@
using System;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.CreateSchemaField>;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class CreateSchema : SchemaCommand, IAppCommand
public sealed class CreateSchema : UpsertCommand, IAppCommand
{
public NamedId<Guid> AppId { get; set; }
public string Name { get; set; }
public SchemaFields Fields { get; set; }
public SchemaProperties Properties { get; set; }
public bool Singleton { get; set; }
public bool Publish { get; set; }
public bool IsSingleton { get; set; }
public CreateSchema()
{
SchemaId = Guid.NewGuid();
}
public Schema ToSchema()
{
return ToSchema(Name, IsSingleton);
}
}
}

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

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

7
src/Squidex/Areas/Api/Controllers/Schemas/Models/PreviewUrlsDto.cs → src/Squidex.Domain.Apps.Entities/Schemas/Commands/ParentFieldCommand.cs

@ -5,11 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class PreviewUrlsDto : Dictionary<string, string>
public abstract class ParentFieldCommand : SchemaCommand
{
public long? ParentFieldId { get; set; }
}
}

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

@ -9,10 +9,8 @@ using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class ReorderFields : SchemaCommand
public sealed class ReorderFields : ParentFieldCommand
{
public long? ParentFieldId { get; set; }
public List<long> FieldIds { get; set; }
}
}

16
src/Squidex.Domain.Apps.Entities/Schemas/Commands/SynchronizeSchema.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class SynchronizeSchema : UpsertCommand
{
public bool NoFieldDeletion { get; set; }
public bool NoFieldRecreation { get; set; }
}
}

116
src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs

@ -0,0 +1,116 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Entities.Schemas.Commands.UpsertSchemaField>;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public abstract class UpsertCommand : SchemaCommand
{
public bool IsPublished { get; set; }
public string Category { get; set; }
public SchemaFields Fields { get; set; }
public SchemaScripts Scripts { get; set; }
public SchemaProperties Properties { get; set; }
public Dictionary<string, string> PreviewUrls { get; set; }
public Schema ToSchema(string name, bool isSingleton)
{
var schema = new Schema(name, Properties, isSingleton);
if (IsPublished)
{
schema = schema.Publish();
}
if (Scripts != null)
{
schema = schema.ConfigureScripts(Scripts);
}
if (PreviewUrls != null)
{
schema = schema.ConfigurePreviewUrls(PreviewUrls);
}
if (!string.IsNullOrWhiteSpace(Category))
{
schema = schema.ChangeCategory(Category);
}
var totalFields = 0;
if (Fields != null)
{
foreach (var eventField in Fields)
{
totalFields++;
var partitioning = Partitioning.FromString(eventField.Partitioning);
var field = eventField.Properties.CreateRootField(totalFields, eventField.Name, partitioning);
if (field is ArrayField arrayField && eventField.Nested?.Count > 0)
{
foreach (var nestedEventField in eventField.Nested)
{
totalFields++;
var nestedField = nestedEventField.Properties.CreateNestedField(totalFields, nestedEventField.Name);
if (nestedEventField.IsHidden)
{
nestedField = nestedField.Hide();
}
if (nestedEventField.IsDisabled)
{
nestedField = nestedField.Disable();
}
if (nestedEventField.IsLocked)
{
nestedField = nestedField.Lock();
}
arrayField = arrayField.AddField(nestedField);
}
field = arrayField;
}
if (eventField.IsHidden)
{
field = field.Hide();
}
if (eventField.IsDisabled)
{
field = field.Disable();
}
if (eventField.IsLocked)
{
field = field.Lock();
}
schema = schema.AddField(field);
}
}
return schema;
}
}
}

7
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaField.cs → src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaField.cs

@ -6,13 +6,14 @@
// ==========================================================================
using System.Collections.Generic;
using P = Squidex.Domain.Apps.Core.Partitioning;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class CreateSchemaField : CreateSchemaFieldBase
public sealed class UpsertSchemaField : UpsertSchemaFieldBase
{
public string Partitioning { get; set; } = "invariant";
public string Partitioning { get; set; } = P.Invariant.Key;
public List<CreateSchemaNestedField> Nested { get; set; }
public List<UpsertSchemaNestedField> Nested { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaFieldBase.cs → src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaFieldBase.cs

@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public abstract class CreateSchemaFieldBase
public abstract class UpsertSchemaFieldBase
{
public string Name { get; set; }

2
src/Squidex.Domain.Apps.Entities/Schemas/Commands/CreateSchemaNestedField.cs → src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertSchemaNestedField.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{
public sealed class CreateSchemaNestedField : CreateSchemaFieldBase
public sealed class UpsertSchemaNestedField : UpsertSchemaFieldBase
{
}
}

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

@ -32,60 +32,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
e("A schema with the same name already exists.");
}
if (command.Fields?.Count > 0)
{
var fieldIndex = 0;
var fieldPrefix = string.Empty;
foreach (var field in command.Fields)
{
fieldIndex++;
fieldPrefix = $"Fields[{fieldIndex}]";
if (!field.Partitioning.IsValidPartitioning())
{
e(Not.Valid("Partitioning"), $"{fieldPrefix}.{nameof(field.Partitioning)}");
}
ValidateField(e, fieldPrefix, field);
if (field.Nested?.Count > 0)
{
if (field.Properties is ArrayFieldProperties)
{
var nestedIndex = 0;
var nestedPrefix = string.Empty;
foreach (var nestedField in field.Nested)
{
nestedIndex++;
nestedPrefix = $"{fieldPrefix}.Nested[{nestedIndex}]";
if (nestedField.Properties is ArrayFieldProperties)
{
e("Nested field cannot be array fields.", $"{nestedPrefix}.{nameof(nestedField.Properties)}");
}
ValidateField(e, nestedPrefix, nestedField);
}
}
else if (field.Nested.Count > 0)
{
e("Only array fields can have nested fields.", $"{fieldPrefix}.{nameof(field.Partitioning)}");
}
ValidateUpsert(command, e);
});
}
if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count)
{
e("Fields cannot have duplicate names.", $"{fieldPrefix}.Nested");
}
}
}
public static void CanSynchronize(SynchronizeSchema command)
{
Guard.NotNull(command, nameof(command));
if (command.Fields.Select(x => x.Name).Distinct().Count() != command.Fields.Count)
{
e("Fields cannot have duplicate names.", nameof(command.Fields));
}
}
Validate.It(() => "Cannot synchronize schema.", e =>
{
ValidateUpsert(command, e);
});
}
@ -171,7 +128,65 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
Guard.NotNull(command, nameof(command));
}
private static void ValidateField(AddValidation e, string prefix, CreateSchemaFieldBase field)
private static void ValidateUpsert(UpsertCommand command, AddValidation e)
{
if (command.Fields?.Count > 0)
{
var fieldIndex = 0;
var fieldPrefix = string.Empty;
foreach (var field in command.Fields)
{
fieldIndex++;
fieldPrefix = $"Fields[{fieldIndex}]";
if (!field.Partitioning.IsValidPartitioning())
{
e(Not.Valid("Partitioning"), $"{fieldPrefix}.{nameof(field.Partitioning)}");
}
ValidateField(field, fieldPrefix, e);
if (field.Nested?.Count > 0)
{
if (field.Properties is ArrayFieldProperties)
{
var nestedIndex = 0;
var nestedPrefix = string.Empty;
foreach (var nestedField in field.Nested)
{
nestedIndex++;
nestedPrefix = $"{fieldPrefix}.Nested[{nestedIndex}]";
if (nestedField.Properties is ArrayFieldProperties)
{
e("Nested field cannot be array fields.", $"{nestedPrefix}.{nameof(nestedField.Properties)}");
}
ValidateField(nestedField, nestedPrefix, e);
}
}
else if (field.Nested.Count > 0)
{
e("Only array fields can have nested fields.", $"{fieldPrefix}.{nameof(field.Partitioning)}");
}
if (field.Nested.Select(x => x.Name).Distinct().Count() != field.Nested.Count)
{
e("Fields cannot have duplicate names.", $"{fieldPrefix}.Nested");
}
}
}
if (command.Fields.Select(x => x.Name).Distinct().Count() != command.Fields.Count)
{
e("Fields cannot have duplicate names.", nameof(command.Fields));
}
}
}
private static void ValidateField(UpsertSchemaFieldBase field, string prefix, AddValidation e)
{
if (!field.Name.IsPropertyName())
{

21
src/Squidex.Domain.Apps.Entities/Schemas/ISchemaEntity.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
@ -20,28 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{
NamedId<Guid> AppId { get; }
string Name { get; }
string Category { get; }
bool IsSingleton { get; }
bool IsPublished { get; }
bool IsDeleted { get; }
string ScriptQuery { get; }
string ScriptCreate { get; }
string ScriptUpdate { get; }
string ScriptDelete { get; }
string ScriptChange { get; }
Schema SchemaDef { get; }
Dictionary<string, string> PreviewUrls { get; }
}
}

27
src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs

@ -8,6 +8,7 @@
using System;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using StaticNamedId = Squidex.Infrastructure.NamedId;
namespace Squidex.Domain.Apps.Entities.Schemas
{
@ -15,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{
public static NamedId<Guid> NamedId(this ISchemaEntity schema)
{
return new NamedId<Guid>(schema.Id, schema.Name);
return StaticNamedId.Of(schema.Id, schema.SchemaDef.Name);
}
public static string EscapePartition(this string value)
@ -23,34 +24,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas
return value.Replace('-', '_');
}
public static string TypeName(this IField field)
{
return field.Name.ToPascalCase();
}
public static string TypeName(this ISchemaEntity schema)
{
return schema.SchemaDef.Name.ToPascalCase();
}
public static string DisplayName(this IField field)
{
return field.RawProperties.Label.WithFallback(field.TypeName());
return schema.SchemaDef.TypeName();
}
public static string DisplayName(this ISchemaEntity schema)
{
return schema.SchemaDef.Properties.Label.WithFallback(schema.TypeName());
}
public static string TypeName(this Schema schema)
{
return schema.Name.ToPascalCase();
}
public static string DisplayName(this Schema schema)
{
return schema.Properties.Label.WithFallback(schema.TypeName());
return schema.SchemaDef.DisplayName();
}
}
}

142
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -6,8 +6,8 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.EventSynchronization;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Guards;
@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
@ -27,17 +28,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public sealed class SchemaGrain : SquidexDomainObjectGrain<SchemaState>, ISchemaGrain
{
private readonly IAppProvider appProvider;
private readonly FieldRegistry registry;
private readonly IJsonSerializer serializer;
public SchemaGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, FieldRegistry registry)
public SchemaGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, IJsonSerializer serializer)
: base(store, log)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(registry, nameof(registry));
Guard.NotNull(serializer, nameof(serializer));
this.appProvider = appProvider;
this.registry = registry;
this.serializer = serializer;
}
protected override Task<object> ExecuteAsync(IAggregateCommand command)
@ -75,6 +76,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas
Create(c);
});
case SynchronizeSchema synchronizeSchema:
return UpdateAsync(synchronizeSchema, c =>
{
GuardSchema.CanSynchronize(c);
Synchronize(c);
});
case DeleteField deleteField:
return UpdateAsync(deleteField, c =>
{
@ -200,45 +209,38 @@ namespace Squidex.Domain.Apps.Entities.Schemas
}
}
public void Create(CreateSchema command)
public void Synchronize(SynchronizeSchema command)
{
var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name) });
if (command.Fields != null)
var options = new SchemaSynchronizationOptions
{
@event.Fields = new List<SchemaCreatedField>();
NoFieldDeletion = command.NoFieldDeletion,
NoFieldRecreation = command.NoFieldRecreation
};
foreach (var commandField in command.Fields)
{
var eventField = SimpleMapper.Map(commandField, new SchemaCreatedField());
var schemaSource = Snapshot.SchemaDef;
var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton);
@event.Fields.Add(eventField);
var @events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options);
if (commandField.Nested != null)
{
eventField.Nested = new List<SchemaCreatedNestedField>();
foreach (var nestedField in commandField.Nested)
{
var eventNestedField = SimpleMapper.Map(nestedField, new SchemaCreatedNestedField());
eventField.Nested.Add(eventNestedField);
}
}
}
foreach (var @event in @events)
{
RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event));
}
}
RaiseEvent(@event);
public void Create(CreateSchema command)
{
RaiseEvent(command, new SchemaCreated { SchemaId = NamedId.Of(command.SchemaId, command.Name), Schema = command.ToSchema() });
}
public void Add(AddField command)
{
RaiseEvent(SimpleMapper.Map(command, new FieldAdded { ParentFieldId = GetFieldId(command.ParentFieldId), FieldId = CreateFieldId(command) }));
RaiseEvent(command, new FieldAdded { FieldId = CreateFieldId(command) });
}
public void UpdateField(UpdateField command)
{
RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated()));
RaiseEvent(command, new FieldUpdated());
}
public void LockField(LockField command)
@ -273,88 +275,89 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public void Reorder(ReorderFields command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered { ParentFieldId = GetFieldId(command.ParentFieldId) }));
RaiseEvent(command, new SchemaFieldsReordered());
}
public void Publish(PublishSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaPublished()));
RaiseEvent(command, new SchemaPublished());
}
public void Unpublish(UnpublishSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished()));
RaiseEvent(command, new SchemaUnpublished());
}
public void ConfigureScripts(ConfigureScripts command)
{
RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured()));
RaiseEvent(command, new SchemaScriptsConfigured());
}
public void ChangeCategory(ChangeCategory command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaCategoryChanged()));
RaiseEvent(command, new SchemaCategoryChanged());
}
public void ConfigurePreviewUrls(ConfigurePreviewUrls command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaPreviewUrlsConfigured()));
RaiseEvent(command, new SchemaPreviewUrlsConfigured());
}
public void Delete(DeleteSchema command)
public void Update(UpdateSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted()));
RaiseEvent(command, new SchemaUpdated());
}
public void Update(UpdateSchema command)
public void Delete(DeleteSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated()));
RaiseEvent(command, new SchemaDeleted());
}
private void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event)
private void RaiseEvent<TCommand, TEvent>(TCommand command, TEvent @event) where TCommand : SchemaCommand where TEvent : SchemaEvent
{
SimpleMapper.Map(fieldCommand, @event);
SimpleMapper.Map(command, @event);
if (fieldCommand.ParentFieldId.HasValue)
NamedId<long> GetFieldId(long? id)
{
if (Snapshot.SchemaDef.FieldsById.TryGetValue(fieldCommand.ParentFieldId.Value, out var field))
if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field))
{
@event.ParentFieldId = NamedId.Of(field.Id, field.Name);
return NamedId.Of(field.Id, field.Name);
}
if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fieldCommand.FieldId, out var nestedField))
return null;
}
if (command is ParentFieldCommand pc && @event is ParentFieldEvent pe)
{
if (pc.ParentFieldId.HasValue)
{
if (Snapshot.SchemaDef.FieldsById.TryGetValue(pc.ParentFieldId.Value, out var field))
{
@event.FieldId = NamedId.Of(nestedField.Id, nestedField.Name);
pe.ParentFieldId = NamedId.Of(field.Id, field.Name);
if (command is FieldCommand fc && @event is FieldEvent fe)
{
if (field is IArrayField arrayField && arrayField.FieldsById.TryGetValue(fc.FieldId, out var nestedField))
{
fe.FieldId = NamedId.Of(nestedField.Id, nestedField.Name);
}
}
}
}
}
else
{
@event.FieldId = GetFieldId(fieldCommand.FieldId);
else if (command is FieldCommand fc && @event is FieldEvent fe)
{
fe.FieldId = GetFieldId(fc.FieldId);
}
}
RaiseEvent(@event);
}
private NamedId<long> CreateFieldId(AddField command)
{
return NamedId.Of(Snapshot.TotalFields + 1L, command.Name);
}
private NamedId<long> GetFieldId(long? id)
{
if (id.HasValue && Snapshot.SchemaDef.FieldsById.TryGetValue(id.Value, out var field))
{
return NamedId.Of(field.Id, field.Name);
}
return null;
}
private void RaiseEvent(SchemaEvent @event)
{
if (@event.SchemaId == null)
{
@event.SchemaId = NamedId.Of(Snapshot.Id, Snapshot.Name);
@event.SchemaId = NamedId.Of(Snapshot.Id, Snapshot.SchemaDef.Name);
}
if (@event.AppId == null)
@ -365,6 +368,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas
RaiseEvent(Envelope.Create(@event));
}
private NamedId<long> CreateFieldId(AddField command)
{
return NamedId.Of(Snapshot.SchemaFieldsTotal + 1, command.Name);
}
private void VerifyNotDeleted()
{
if (Snapshot.IsDeleted)
@ -375,7 +383,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
protected override SchemaState OnEvent(Envelope<IEvent> @event)
{
return Snapshot.Apply(@event, registry);
return Snapshot.Apply(@event);
}
public Task<J<ISchemaEntity>> GetStateAsync()

12
src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs

@ -19,6 +19,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("SchemaCreatedEvent",
"created schema {[Name]}.");
AddEventMessage("ScriptsConfiguredEvent",
"configured script of schema {[Name]}.");
AddEventMessage<SchemaFieldsReordered>(
"reordered fields of schema {[Name]}.");
AddEventMessage<SchemaCreated>(
"created schema {[Name]}.");
@ -37,6 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas
AddEventMessage<SchemaFieldsReordered>(
"reordered fields of schema {[Name]}.");
AddEventMessage<SchemaScriptsConfigured>(
"configured script of schema {[Name]}.");
AddEventMessage<FieldAdded>(
"added field {[Field]} to schema {[Name]}.");

165
src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
@ -15,7 +14,6 @@ using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas.State
@ -27,128 +25,27 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
public NamedId<Guid> AppId { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public string Category { get; set; }
[DataMember]
public int TotalFields { get; set; }
public long SchemaFieldsTotal { get; set; }
[DataMember]
public bool IsDeleted { get; set; }
[DataMember]
public bool IsSingleton { get; set; }
[DataMember]
public string ScriptQuery { get; set; }
[DataMember]
public string ScriptCreate { get; set; }
[DataMember]
public string ScriptUpdate { get; set; }
[DataMember]
public string ScriptDelete { get; set; }
[DataMember]
public string ScriptChange { get; set; }
[DataMember]
public Dictionary<string, string> PreviewUrls { get; set; }
[DataMember]
public Schema SchemaDef { get; set; }
[IgnoreDataMember]
public bool IsPublished
protected void On(SchemaCreated @event)
{
get { return SchemaDef.IsPublished; }
}
protected void On(SchemaCreated @event, FieldRegistry registry)
{
Name = @event.Name;
IsSingleton = @event.Singleton;
var schema = new Schema(@event.Name);
if (@event.Properties != null)
{
schema = schema.Update(@event.Properties);
}
if (@event.Publish)
{
schema = schema.Publish();
}
if (@event.Fields != null)
{
foreach (var eventField in @event.Fields)
{
TotalFields++;
var partitioning = Partitioning.FromString(eventField.Partitioning);
var field = registry.CreateRootField(TotalFields, eventField.Name, partitioning, eventField.Properties);
if (field is ArrayField arrayField && eventField.Nested?.Count > 0)
{
foreach (var nestedEventField in eventField.Nested)
{
TotalFields++;
var nestedField = registry.CreateNestedField(TotalFields, nestedEventField.Name, nestedEventField.Properties);
if (nestedEventField.IsHidden)
{
nestedField = nestedField.Hide();
}
if (nestedEventField.IsDisabled)
{
nestedField = nestedField.Disable();
}
arrayField = arrayField.AddField(nestedField);
}
field = arrayField;
}
if (eventField.IsHidden)
{
field = field.Hide();
}
if (eventField.IsDisabled)
{
field = field.Disable();
}
if (eventField.IsLocked)
{
field = field.Lock();
}
schema = schema.AddField(field);
}
}
SchemaDef = schema;
SchemaDef = @event.Schema;
SchemaFieldsTotal = @event.Schema.MaxId();
AppId = @event.AppId;
}
protected void On(FieldAdded @event, FieldRegistry registry)
protected void On(FieldAdded @event)
{
if (@event.ParentFieldId != null)
{
var field = registry.CreateNestedField(@event.FieldId.Id, @event.Name, @event.Properties);
var field = @event.Properties.CreateNestedField(@event.FieldId.Id, @event.Name);
SchemaDef = SchemaDef.UpdateField(@event.ParentFieldId.Id, x => ((ArrayField)x).AddField(field));
}
@ -156,95 +53,95 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State
{
var partitioning = Partitioning.FromString(@event.Partitioning);
var field = registry.CreateRootField(@event.FieldId.Id, @event.Name, partitioning, @event.Properties);
var field = @event.Properties.CreateRootField(@event.FieldId.Id, @event.Name, partitioning);
SchemaDef = SchemaDef.DeleteField(@event.FieldId.Id);
SchemaDef = SchemaDef.AddField(field);
}
TotalFields++;
SchemaFieldsTotal++;
}
protected void On(SchemaCategoryChanged @event, FieldRegistry registry)
protected void On(SchemaCategoryChanged @event)
{
Category = @event.Name;
SchemaDef = SchemaDef.ChangeCategory(@event.Name);
}
protected void On(SchemaPreviewUrlsConfigured @event, FieldRegistry registry)
protected void On(SchemaPreviewUrlsConfigured @event)
{
PreviewUrls = @event.PreviewUrls;
SchemaDef = SchemaDef.ConfigurePreviewUrls(@event.PreviewUrls);
}
protected void On(SchemaPublished @event, FieldRegistry registry)
protected void On(SchemaScriptsConfigured @event)
{
SchemaDef = SchemaDef.ConfigureScripts(@event.Scripts);
}
protected void On(SchemaPublished @event)
{
SchemaDef = SchemaDef.Publish();
}
protected void On(SchemaUnpublished @event, FieldRegistry registry)
protected void On(SchemaUnpublished @event)
{
SchemaDef = SchemaDef.Unpublish();
}
protected void On(SchemaUpdated @event, FieldRegistry registry)
protected void On(SchemaUpdated @event)
{
SchemaDef = SchemaDef.Update(@event.Properties);
}
protected void On(SchemaFieldsReordered @event, FieldRegistry registry)
protected void On(SchemaFieldsReordered @event)
{
SchemaDef = SchemaDef.ReorderFields(@event.FieldIds, @event.ParentFieldId?.Id);
}
protected void On(FieldUpdated @event, FieldRegistry registry)
protected void On(FieldUpdated @event)
{
SchemaDef = SchemaDef.UpdateField(@event.FieldId.Id, @event.Properties, @event.ParentFieldId?.Id);
}
protected void On(FieldLocked @event, FieldRegistry registry)
protected void On(FieldLocked @event)
{
SchemaDef = SchemaDef.LockField(@event.FieldId.Id, @event.ParentFieldId?.Id);
}
protected void On(FieldDisabled @event, FieldRegistry registry)
protected void On(FieldDisabled @event)
{
SchemaDef = SchemaDef.DisableField(@event.FieldId.Id, @event.ParentFieldId?.Id);
}
protected void On(FieldEnabled @event, FieldRegistry registry)
protected void On(FieldEnabled @event)
{
SchemaDef = SchemaDef.EnableField(@event.FieldId.Id, @event.ParentFieldId?.Id);
}
protected void On(FieldHidden @event, FieldRegistry registry)
protected void On(FieldHidden @event)
{
SchemaDef = SchemaDef.HideField(@event.FieldId.Id, @event.ParentFieldId?.Id);
}
protected void On(FieldShown @event, FieldRegistry registry)
protected void On(FieldShown @event)
{
SchemaDef = SchemaDef.ShowField(@event.FieldId.Id, @event.ParentFieldId?.Id);
}
protected void On(FieldDeleted @event, FieldRegistry registry)
protected void On(FieldDeleted @event)
{
SchemaDef = SchemaDef.DeleteField(@event.FieldId.Id, @event.ParentFieldId?.Id);
}
protected void On(SchemaDeleted @event, FieldRegistry registry)
protected void On(SchemaDeleted @event)
{
IsDeleted = true;
}
protected void On(ScriptsConfigured @event, FieldRegistry registry)
{
SimpleMapper.Map(@event, this);
}
public SchemaState Apply(Envelope<IEvent> @event, FieldRegistry registry)
public SchemaState Apply(Envelope<IEvent> @event)
{
var payload = (SquidexEvent)@event.Payload;
return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload, registry));
return Clone().Update(payload, @event.Headers, r => r.DispatchAction(payload));
}
}
}

2
src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.0">
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

4
src/Squidex.Domain.Apps.Events/Schemas/FieldEvent.cs

@ -9,10 +9,8 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
public abstract class FieldEvent : SchemaEvent
public abstract class FieldEvent : ParentFieldEvent
{
public NamedId<long> FieldId { get; set; }
public NamedId<long> ParentFieldId { get; set; }
}
}

16
src/Squidex.Domain.Apps.Events/Schemas/ParentFieldEvent.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
public abstract class ParentFieldEvent : SchemaEvent
{
public NamedId<long> ParentFieldId { get; set; }
}
}

15
src/Squidex.Domain.Apps.Events/Schemas/SchemaCreated.cs

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
@ -7,21 +7,12 @@
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.EventSourcing;
using SchemaFields = System.Collections.Generic.List<Squidex.Domain.Apps.Events.Schemas.SchemaCreatedField>;
namespace Squidex.Domain.Apps.Events.Schemas
{
[EventType(nameof(SchemaCreated))]
[EventType(nameof(SchemaCreated), 2)]
public sealed class SchemaCreated : SchemaEvent
{
public string Name { get; set; }
public SchemaFields Fields { get; set; }
public SchemaProperties Properties { get; set; }
public bool Singleton { get; set; }
public bool Publish { get; set; }
public Schema Schema { get; set; }
}
}

5
src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldsReordered.cs

@ -6,16 +6,13 @@
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Schemas
{
[EventType(nameof(SchemaFieldsReordered))]
public sealed class SchemaFieldsReordered : SchemaEvent
public sealed class SchemaFieldsReordered : ParentFieldEvent
{
public NamedId<long> ParentFieldId { get; set; }
public List<long> FieldIds { get; set; }
}
}

15
src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs → src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs

@ -5,21 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Schemas
{
[EventType(nameof(ScriptsConfigured))]
public sealed class ScriptsConfigured : SchemaEvent
[EventType(nameof(SchemaScriptsConfigured))]
public sealed class SchemaScriptsConfigured : SchemaEvent
{
public string ScriptQuery { get; set; }
public string ScriptCreate { get; set; }
public string ScriptUpdate { get; set; }
public string ScriptDelete { get; set; }
public string ScriptChange { get; set; }
public SchemaScripts Scripts { get; set; }
}
}

2
src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj

@ -17,7 +17,7 @@
<PackageReference Include="IdentityServer4" Version="2.3.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="MongoDB.Driver" Version="2.7.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Security.Principal.Windows" Version="4.5.1" />

4
src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj

@ -12,8 +12,8 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.7.2" />
<PackageReference Include="MongoDB.Driver" Version="2.7.3" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.7.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" />

4
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -12,12 +12,12 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.0">
<PackageReference Include="Microsoft.Orleans.CodeGenerator.MSBuild" Version="2.2.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="NodaTime" Version="2.4.4" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

22
src/Squidex/Areas/Api/Config/Swagger/XmlResponseTypesProcessor.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NJsonSchema.Infrastructure;
@ -23,8 +24,6 @@ namespace Squidex.Areas.Api.Config.Swagger
public async Task<bool> ProcessAsync(OperationProcessorContext context)
{
var hasOkResponse = false;
var operation = context.OperationDescription.Operation;
var returnsDescription = await context.MethodInfo.GetXmlDocumentationTagAsync("returns") ?? string.Empty;
@ -41,19 +40,11 @@ namespace Squidex.Areas.Api.Config.Swagger
}
response.Description = match.Groups["Description"].Value;
if (statusCode == "200" || statusCode == "204")
{
hasOkResponse = true;
}
}
await AddInternalErrorResponseAsync(context, operation);
if (!hasOkResponse)
{
RemoveOkResponse(operation);
}
CleanupResponses(operation);
return true;
}
@ -66,11 +57,14 @@ namespace Squidex.Areas.Api.Config.Swagger
}
}
private static void RemoveOkResponse(SwaggerOperation operation)
private static void CleanupResponses(SwaggerOperation operation)
{
if (operation.Responses.TryGetValue("200", out var response) && response.Description?.Contains("=>") == true)
foreach (var (code, response) in operation.Responses.ToList())
{
operation.Responses.Remove("200");
if (string.IsNullOrWhiteSpace(response.Description) || response.Description?.Contains("=>") == true)
{
operation.Responses.Remove(code);
}
}
}
}

2
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs

@ -230,7 +230,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
});
}
private SwaggerPathItem AddOperation(SwaggerOperationMethod method, string entityName, string path, Action<SwaggerOperation> updater)
private SwaggerPathItem AddOperation(string method, string entityName, string path, Action<SwaggerOperation> updater)
{
var operations = document.Paths.GetOrAddNew(path);
var operation = new SwaggerOperation();

2
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasSwaggerGenerator.cs

@ -77,7 +77,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
var appBasePath = $"/content/{app.Name}";
foreach (var schema in schemas.Where(x => x.IsPublished).Select(x => x.SchemaDef))
foreach (var schema in schemas.Select(x => x.SchemaDef).Where(x => x.IsPublished))
{
new SchemaSwaggerGenerator(document, app.Name, appBasePath, schema, AppendSchema, app.PartitionResolver()).GenerateSchemaOperations();
}

20
src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigurePreviewUrlsDto.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;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class ConfigurePreviewUrlsDto : Dictionary<string, string>
{
public ConfigurePreviewUrls ToCommand()
{
return new ConfigurePreviewUrls { PreviewUrls = this };
}
}
}

61
src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaDto.cs

@ -5,15 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class CreateSchemaDto
public sealed class CreateSchemaDto : UpsertDto
{
/// <summary>
/// The name of the schema.
@ -22,66 +19,14 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; }
/// <summary>
/// The optional properties.
/// </summary>
public SchemaPropertiesDto Properties { get; set; }
/// <summary>
/// Optional fields.
/// </summary>
public List<CreateSchemaFieldDto> Fields { get; set; }
/// <summary>
/// Set to true to allow a single content item only.
/// </summary>
public bool Singleton { get; set; }
/// <summary>
/// Set it to true to autopublish the schema.
/// </summary>
public bool Publish { get; set; }
public bool IsSingleton { get; set; }
public CreateSchema ToCommand()
{
var command = new CreateSchema();
SimpleMapper.Map(this, command);
if (Properties != null)
{
command.Properties = new SchemaProperties();
SimpleMapper.Map(Properties, command.Properties);
}
if (Fields != null)
{
command.Fields = new List<CreateSchemaField>();
foreach (var fieldDto in Fields)
{
var rootProperties = fieldDto?.Properties.ToProperties();
var rootField = SimpleMapper.Map(fieldDto, new CreateSchemaField { Properties = rootProperties });
if (fieldDto.Nested != null)
{
rootField.Nested = new List<CreateSchemaNestedField>();
foreach (var nestedFieldDto in fieldDto.Nested)
{
var nestedProperties = nestedFieldDto?.Properties.ToProperties();
var nestedField = SimpleMapper.Map(nestedFieldDto, new CreateSchemaNestedField { Properties = nestedProperties });
rootField.Nested.Add(nestedField);
}
}
command.Fields.Add(rootField);
}
}
return command;
return ToCommand(this, new CreateSchema());
}
}
}

38
src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs

@ -19,6 +19,8 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class SchemaDetailsDto
{
private static readonly Dictionary<string, string> EmptyPreviewUrls = new Dictionary<string, string>();
/// <summary>
/// The id of the schema.
/// </summary>
@ -47,34 +49,14 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
public bool IsPublished { get; set; }
/// <summary>
/// The script that is executed for each query when querying contents.
/// </summary>
public string ScriptQuery { get; set; }
/// <summary>
/// The script that is executed when creating a content.
/// </summary>
public string ScriptCreate { get; set; }
/// <summary>
/// The script that is executed when updating a content.
/// </summary>
public string ScriptUpdate { get; set; }
/// <summary>
/// The script that is executed when deleting a content.
/// </summary>
public string ScriptDelete { get; set; }
/// <summary>
/// The script that is executed when changing a content status.
/// The scripts.
/// </summary>
public string ScriptChange { get; set; }
public SchemaScriptsDto Scripts { get; set; } = new SchemaScriptsDto();
/// <summary>
/// The preview Urls.
/// </summary>
public Dictionary<string, string> PreviewUrls { get; set; }
public Dictionary<string, string> PreviewUrls { get; set; } = EmptyPreviewUrls;
/// <summary>
/// The list of fields.
@ -86,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// The schema properties.
/// </summary>
[Required]
public SchemaPropertiesDto Properties { get; set; }
public SchemaPropertiesDto Properties { get; set; } = new SchemaPropertiesDto();
/// <summary>
/// The user that has created the schema.
@ -117,12 +99,18 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
public static SchemaDetailsDto FromSchema(ISchemaEntity schema)
{
var response = new SchemaDetailsDto { Properties = new SchemaPropertiesDto() };
var response = new SchemaDetailsDto();
SimpleMapper.Map(schema, response);
SimpleMapper.Map(schema.SchemaDef, response);
SimpleMapper.Map(schema.SchemaDef.Scripts, response.Scripts);
SimpleMapper.Map(schema.SchemaDef.Properties, response.Properties);
if (schema.SchemaDef.PreviewUrls.Count > 0)
{
response.PreviewUrls = new Dictionary<string, string>(schema.SchemaDef.PreviewUrls);
}
response.Fields = new List<FieldDto>();
foreach (var field in schema.SchemaDef.Fields)

17
src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureScriptsDto.cs → src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaScriptsDto.cs

@ -5,41 +5,44 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class ConfigureScriptsDto
public sealed class SchemaScriptsDto
{
/// <summary>
/// The script that is executed for each query when querying contents.
/// </summary>
public string ScriptQuery { get; set; }
public string Query { get; set; }
/// <summary>
/// The script that is executed when creating a content.
/// </summary>
public string ScriptCreate { get; set; }
public string Create { get; set; }
/// <summary>
/// The script that is executed when updating a content.
/// </summary>
public string ScriptUpdate { get; set; }
public string Update { get; set; }
/// <summary>
/// The script that is executed when deleting a content.
/// </summary>
public string ScriptDelete { get; set; }
public string Delete { get; set; }
/// <summary>
/// The script that is executed when change a content status.
/// </summary>
public string ScriptChange { get; set; }
public string Change { get; set; }
public ConfigureScripts ToCommand()
{
return SimpleMapper.Map(this, new ConfigureScripts());
var scripts = SimpleMapper.Map(this, new SchemaScripts());
return new ConfigureScripts { Scripts = scripts };
}
}
}

29
src/Squidex/Areas/Api/Controllers/Schemas/Models/SynchronizeSchemaDto.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Schemas.Commands;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class SynchronizeSchemaDto : UpsertDto
{
/// <summary>
/// True, when fields should not be deleted.
/// </summary>
public bool NoFieldDeletion { get; set; }
/// <summary>
/// True, when fields with different types should not be recreated.
/// </summary>
public bool NoFieldRecreation { get; set; }
public SynchronizeSchema ToCommand()
{
return ToCommand(this, new SynchronizeSchema());
}
}
}

98
src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertDto.cs

@ -0,0 +1,98 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public abstract class UpsertDto
{
/// <summary>
/// The optional properties.
/// </summary>
public SchemaPropertiesDto Properties { get; set; }
/// <summary>
/// The optional scripts.
/// </summary>
public SchemaScriptsDto Scripts { get; set; }
/// <summary>
/// Optional fields.
/// </summary>
public List<UpsertSchemaFieldDto> Fields { get; set; }
/// <summary>
/// The optional preview urls.
/// </summary>
public Dictionary<string, string> PreviewUrls { get; set; }
/// <summary>
/// The category.
/// </summary>
public string Category { get; set; }
/// <summary>
/// Set it to true to autopublish the schema.
/// </summary>
public bool IsPublished { get; set; }
public static TCommand ToCommand<TCommand, TDto>(TDto dto, TCommand command) where TCommand : UpsertCommand where TDto : UpsertDto
{
SimpleMapper.Map(dto, command);
if (dto.Properties != null)
{
command.Properties = new SchemaProperties();
SimpleMapper.Map(dto.Properties, command.Properties);
}
if (dto.Scripts != null)
{
command.Scripts = new SchemaScripts();
SimpleMapper.Map(dto.Scripts, command.Scripts);
}
if (dto.Fields != null)
{
command.Fields = new List<UpsertSchemaField>();
foreach (var rootFieldDto in dto.Fields)
{
var rootProps = rootFieldDto?.Properties.ToProperties();
var rootField = new UpsertSchemaField { Properties = rootProps };
SimpleMapper.Map(rootFieldDto, rootField);
if (rootFieldDto.Nested?.Count > 0)
{
rootField.Nested = new List<UpsertSchemaNestedField>();
foreach (var nestedFieldDto in rootFieldDto.Nested)
{
var nestedProps = nestedFieldDto?.Properties.ToProperties();
var nestedField = new UpsertSchemaNestedField { Properties = nestedProps };
SimpleMapper.Map(nestedFieldDto, nestedField);
rootField.Nested.Add(nestedField);
}
}
command.Fields.Add(rootField);
}
}
return command;
}
}
}

4
src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaFieldDto.cs → src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaFieldDto.cs

@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class CreateSchemaFieldDto
public sealed class UpsertSchemaFieldDto
{
/// <summary>
/// The name of the field. Must be unique within the schema.
@ -48,6 +48,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
/// <summary>
/// The nested fields.
/// </summary>
public List<CreateSchemaNestedFieldDto> Nested { get; set; }
public List<UpsertSchemaNestedFieldDto> Nested { get; set; }
}
}

2
src/Squidex/Areas/Api/Controllers/Schemas/Models/CreateSchemaNestedFieldDto.cs → src/Squidex/Areas/Api/Controllers/Schemas/Models/UpsertSchemaNestedFieldDto.cs

@ -9,7 +9,7 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class CreateSchemaNestedFieldDto
public sealed class UpsertSchemaNestedFieldDto
{
/// <summary>
/// The name of the field. Must be unique within the schema.

2
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -146,7 +146,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
@ -172,7 +171,6 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]

36
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -132,7 +132,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param>
/// <returns>
/// 204 => Schema has been updated.
/// 204 => Schema updated.
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
@ -147,6 +147,28 @@ namespace Squidex.Areas.Api.Controllers.Schemas
return NoContent();
}
/// <summary>
/// Synchronize a schema.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param>
/// <returns>
/// 204 => Schema updated.
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/sync")]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request)
{
await CommandBus.PublishAsync(request.ToCommand());
return NoContent();
}
/// <summary>
/// Update a schema category.
/// </summary>
@ -154,7 +176,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param>
/// <returns>
/// 204 => Schema has been updated.
/// 204 => Schema updated.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
@ -175,16 +197,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="request">The preview urls for the schema.</param>
/// <returns>
/// 204 => Schema has been updated.
/// 204 => Schema updated.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/preview-urls")]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutPreviewUrls(string app, string name, [FromBody] PreviewUrlsDto request)
public async Task<IActionResult> PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request)
{
await CommandBus.PublishAsync(new ConfigurePreviewUrls { PreviewUrls = request ?? new PreviewUrlsDto() });
await CommandBus.PublishAsync(request.ToCommand());
return NoContent();
}
@ -196,7 +218,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="request">The schema scripts object that needs to updated.</param>
/// <returns>
/// 204 => Schema has been updated.
/// 204 => Schema updated.
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
@ -204,7 +226,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[Route("apps/{app}/schemas/{name}/scripts/")]
[ApiPermission(Permissions.AppSchemasScripts)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchemaScripts(string app, string name, [FromBody] ConfigureScriptsDto request)
public async Task<IActionResult> PutSchemaScripts(string app, string name, [FromBody] SchemaScriptsDto request)
{
await CommandBus.PublishAsync(request.ToCommand());

3
src/Squidex/Config/Domain/EntitiesServices.cs

@ -251,6 +251,9 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ConvertEventStoreAppId>()
.As<IMigration>();
services.AddTransientAs<ClearSchemas>()
.As<IMigration>();
services.AddTransientAs<PopulateGrainIndexes>()
.As<IMigration>();

4
src/Squidex/Config/Domain/SerializationServices.cs

@ -30,7 +30,6 @@ namespace Squidex.Config.Domain
.MapUnmapped(SquidexEvents.Assembly)
.MapUnmapped(SquidexInfrastructure.Assembly)
.MapUnmapped(SquidexMigrations.Assembly);
private static readonly FieldRegistry FieldRegistry = new FieldRegistry(TypeNameRegistry);
public static readonly JsonSerializerSettings DefaultJsonSettings = new JsonSerializerSettings();
public static readonly JsonSerializer DefaultJsonSerializer;
@ -70,6 +69,8 @@ namespace Squidex.Config.Domain
static SerializationServices()
{
FieldRegistry.Setup(TypeNameRegistry);
ConfigureJson(DefaultJsonSettings, TypeNameHandling.Auto);
DefaultJsonSerializer = JsonSerializer.Create(DefaultJsonSettings);
@ -77,7 +78,6 @@ namespace Squidex.Config.Domain
public static IServiceCollection AddMySerializers(this IServiceCollection services)
{
services.AddSingleton(FieldRegistry);
services.AddSingleton(DefaultJsonSettings);
services.AddSingleton(DefaultJsonSerializer);
services.AddSingleton(TypeNameRegistry);

2
src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs

@ -97,7 +97,7 @@ namespace Squidex.Pipeline.CommandMiddlewares
throw new DomainObjectNotFoundException(schemaName, typeof(ISchemaEntity));
}
return NamedId.Of(schema.Id, schema.Name);
return schema.NamedId();
}
}

2
src/Squidex/Pipeline/UrlGenerator.cs

@ -57,7 +57,7 @@ namespace Squidex.Pipeline
public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content)
{
return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.Name}/{content.Id}");
return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.SchemaDef.Name}/{content.Id}");
}
public string GenerateContentUIUrl(NamedId<Guid> appId, NamedId<Guid> schemaId, Guid contentId)

12
src/Squidex/Squidex.csproj

@ -62,7 +62,7 @@
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="2.0.0" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="2.0.1" />
<PackageReference Include="Ben.BlockingDetector" Version="0.0.3" />
<PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.1.0.23" />
<PackageReference Include="IdentityServer4" Version="2.3.2" />
@ -85,15 +85,15 @@
<PackageReference Include="Microsoft.Orleans.Client" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.2" />
<PackageReference Include="NJsonSchema" Version="9.13.11" />
<PackageReference Include="NSwag.AspNetCore" Version="12.0.9" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.2.3" />
<PackageReference Include="MongoDB.Driver" Version="2.7.3" />
<PackageReference Include="NJsonSchema" Version="9.13.17" />
<PackageReference Include="NSwag.AspNetCore" Version="12.0.13" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" />
<PackageReference Include="OrleansDashboard" Version="2.1.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.0.4" />
<PackageReference Include="ReportGenerator" Version="4.0.9" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.6" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" />

2
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html

@ -7,7 +7,7 @@
<ng-container tabs>
<ul class="nav nav-tabs2">
<li class="nav-item" *ngFor="let script of editForm.form.controls | sqxKeys">
<a class="nav-link" [class.active]="selectedField === script" (click)="selectField(script)">{{script.substr(6)}}</a>
<a class="nav-link" [class.active]="selectedField === script" (click)="selectField(script)">{{script | titlecase}}</a>
</li>
</ul>
</ng-container>

4
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts

@ -26,7 +26,7 @@ export class SchemaScriptsFormComponent implements OnInit {
@Input()
public schema: SchemaDetailsDto;
public selectedField = 'scriptQuery';
public selectedField = 'query';
public editForm = new EditScriptsForm(this.formBuilder);
@ -37,7 +37,7 @@ export class SchemaScriptsFormComponent implements OnInit {
}
public ngOnInit() {
this.editForm.load(this.schema);
this.editForm.load(this.schema.scripts);
}
public complete() {

8
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -34,11 +34,11 @@
<div class="row no-gutters">
<div class="col-6 type">
<label>
<input type="radio" class="radio-input" formControlName="singleton" [value]="false" />
<input type="radio" class="radio-input" formControlName="isSingleton" [value]="false" />
<div class="row no-gutters">
<div class="col-auto">
<div class="type-icon" [class.active]="createForm.form.controls['singleton'].value !== true">
<div class="type-icon" [class.active]="createForm.form.controls['isSingleton'].value !== true">
<i class="icon-multiple-content"></i>
</div>
</div>
@ -52,11 +52,11 @@
</div>
<div class="col-6 type">
<label>
<input type="radio" class="radio-input" formControlName="singleton" [value]="true" />
<input type="radio" class="radio-input" formControlName="isSingleton" [value]="true" />
<div class="row no-gutters">
<div class="col-auto">
<div class="type-icon" [class.active]="createForm.form.controls['singleton'].value === true">
<div class="type-icon" [class.active]="createForm.form.controls['isSingleton'].value === true">
<i class="icon-single-content"></i>
</div>
</div>

2
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts

@ -67,7 +67,7 @@ export class SchemaFormComponent implements OnInit {
const value = this.createForm.submit();
if (value) {
const schemaDto = Object.assign(value.import || {}, { name: value.name, singleton: value.singleton });
const schemaDto = Object.assign(value.import || {}, { name: value.name, isSingleton: value.isSingleton });
this.schemasState.create(schemaDto)
.subscribe(dto => {

31
src/Squidex/app/shared/services/schemas.service.spec.ts

@ -25,7 +25,6 @@ import {
UpdateFieldDto,
UpdateSchemaCategoryDto,
UpdateSchemaDto,
UpdateSchemaScriptsDto,
Version
} from './../';
@ -284,11 +283,13 @@ describe('SchemasService', () => {
}
}
],
scriptQuery: '<script-query>',
scriptCreate: '<script-create>',
scriptUpdate: '<script-update>',
scriptDelete: '<script-delete>',
scriptChange: '<script-change>'
scripts: {
query: '<script-query>',
create: '<script-create>',
change: '<script-change>',
delete: '<script-delete>',
update: '<script-update>'
}
}, {
headers: {
etag: '2'
@ -316,13 +317,15 @@ describe('SchemasService', () => {
new RootFieldDto(20, 'field20', createProperties('Tags'), 'language', true, true, true)
],
{
'Default': 'url'
query: '<script-query>',
create: '<script-create>',
change: '<script-change>',
delete: '<script-delete>',
update: '<script-update>'
},
'<script-query>',
'<script-create>',
'<script-update>',
'<script-delete>',
'<script-change>'));
{
'Default': 'url'
}));
}));
it('should make post request to create schema',
@ -349,7 +352,7 @@ describe('SchemasService', () => {
}
});
expect(schema!).toEqual(new SchemaDetailsDto('1', dto.name, '', new SchemaPropertiesDto(), true, false, now, user, now, user, new Version('2'), [], {}));
expect(schema!).toEqual(new SchemaDetailsDto('1', dto.name, '', new SchemaPropertiesDto(), true, false, now, user, now, user, new Version('2'), [], {}, {}));
}));
it('should make put request to update schema',
@ -370,7 +373,7 @@ describe('SchemasService', () => {
it('should make put request to update schema scripts',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
const dto = new UpdateSchemaScriptsDto();
const dto = {};
schemasService.putScripts('my-app', 'my-schema', dto, version).subscribe();

41
src/Squidex/app/shared/services/schemas.service.ts

@ -63,13 +63,9 @@ export class SchemaDetailsDto extends SchemaDto {
lastModified: DateTime,
lastModifiedBy: string,
version: Version,
public readonly fields: RootFieldDto[],
public readonly previewUrls: { [name: string]: string },
public readonly scriptQuery?: string,
public readonly scriptCreate?: string,
public readonly scriptUpdate?: string,
public readonly scriptDelete?: string,
public readonly scriptChange?: string
public readonly fields: RootFieldDto[] = [],
public readonly scripts = {},
public readonly previewUrls = {}
) {
super(id, name, category, properties, isSingleton, isPublished, created, createdBy, lastModified, lastModifiedBy, version);
@ -215,17 +211,6 @@ export class UpdateSchemaDto {
}
}
export class UpdateSchemaScriptsDto {
constructor(
public readonly scriptQuery?: string,
public readonly scriptCreate?: string,
public readonly scriptUpdate?: string,
public readonly scriptDelete?: string,
public readonly scriptChange?: string
) {
}
}
@Injectable()
export class SchemasService {
constructor(
@ -318,12 +303,8 @@ export class SchemasService {
DateTime.parseISO_UTC(body.lastModified), body.lastModifiedBy,
response.version,
fields,
body.previewUrls || {},
body.scriptQuery,
body.scriptCreate,
body.scriptUpdate,
body.scriptDelete,
body.scriptChange);
body.scripts || {},
body.previewUrls || {});
}),
pretifyError('Failed to load schema. Please reload.'));
}
@ -347,13 +328,7 @@ export class SchemasService {
now, user,
now, user,
response.version,
dto.fields || [],
{},
body.scriptQuery,
body.scriptCreate,
body.scriptUpdate,
body.scriptDelete,
body.scriptChange);
dto.fields || []);
}),
tap(() => {
this.analytics.trackEvent('Schema', 'Created', appName);
@ -371,7 +346,7 @@ export class SchemasService {
pretifyError('Failed to delete schema. Please reload.'));
}
public putScripts(appName: string, schemaName: string, dto: UpdateSchemaScriptsDto, version: Version): Observable<Versioned<any>> {
public putScripts(appName: string, schemaName: string, dto: {}, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/scripts`);
return HTTP.putVersioned(this.http, url, dto, version).pipe(
@ -421,7 +396,7 @@ export class SchemasService {
pretifyError('Failed to change category. Please reload.'));
}
public putPreviewUrls(appName: string, schemaName: string, dto: { [name: string]: string }, version: Version): Observable<Versioned<any>> {
public putPreviewUrls(appName: string, schemaName: string, dto: {}, version: Version): Observable<Versioned<any>> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/preview-urls`);
return HTTP.putVersioned(this.http, url, dto, version).pipe(

2
src/Squidex/app/shared/state/contents.forms.spec.ts

@ -371,7 +371,7 @@ describe('StringField', () => {
});
function createSchema(properties: SchemaPropertiesDto, index = 1, fields: RootFieldDto[]) {
return new SchemaDetailsDto('id' + index, 'schema' + index, '', properties, false, true, null!, null!, null!, null!, null!, fields, {});
return new SchemaDetailsDto('id' + index, 'schema' + index, '', properties, false, true, null!, null!, null!, null!, null!, fields);
}
function createField(properties: FieldPropertiesDto, index = 1, partitioning = 'languages') {

12
src/Squidex/app/shared/state/schemas.forms.ts

@ -40,7 +40,7 @@ export class CreateSchemaForm extends Form<FormGroup> {
ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes only (not at the end).')
]
],
singleton: false,
isSingleton: false,
import: {}
}));
}
@ -136,11 +136,11 @@ export class ConfigurePreviewUrlsForm extends Form<FormArray> {
export class EditScriptsForm extends Form<FormGroup> {
constructor(formBuilder: FormBuilder) {
super(formBuilder.group({
scriptQuery: '',
scriptCreate: '',
scriptUpdate: '',
scriptDelete: '',
scriptChange: ''
query: '',
create: '',
change: '',
delete: '',
update: ''
}));
}
}

32
src/Squidex/app/shared/state/schemas.state.spec.ts

@ -26,7 +26,6 @@ import {
UpdateFieldDto,
UpdateSchemaCategoryDto,
UpdateSchemaDto,
UpdateSchemaScriptsDto,
Version,
Versioned
} from '@app/shared';
@ -56,8 +55,7 @@ describe('SchemasState', () => {
creation, creator,
creation, creator,
version,
[field1, field2],
{});
[field1, field2]);
let dialogs: IMock<DialogService>;
let appsState: IMock<AppsState>;
@ -264,44 +262,38 @@ describe('SchemasState', () => {
expectToBeModified(schema_1);
});
it('should update script properties and update user info when preview urls configured', () => {
const request = {
'Default': 'url'
};
it('should update script properties and update user info when scripts configured', () => {
const request = { query: '<query-script>' };
schemasService.setup(x => x.putPreviewUrls(app, schema.name, It.isAny(), version))
schemasService.setup(x => x.putScripts(app, schema.name, It.isAny(), version))
.returns(() => of(new Versioned<any>(newVersion, {})));
schemasState.configurePreviewUrls(schema, request, modified).subscribe();
schemasState.configureScripts(schema, request, modified).subscribe();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expect(schema_1.previewUrls).toEqual(request);
expect(schema_1.scripts['query']).toEqual('<query-script>');
expectToBeModified(schema_1);
});
it('should update script properties and update user info when scripts configured', () => {
const request = new UpdateSchemaScriptsDto('query', 'create', 'update', 'delete', 'change');
it('should update script properties and update user info when preview urls configured', () => {
const request = { web: 'url' };
schemasService.setup(x => x.putScripts(app, schema.name, It.isAny(), version))
schemasService.setup(x => x.putPreviewUrls(app, schema.name, It.isAny(), version))
.returns(() => of(new Versioned<any>(newVersion, {})));
schemasState.configureScripts(schema, request, modified).subscribe();
schemasState.configurePreviewUrls(schema, request, modified).subscribe();
const schema_1 = <SchemaDetailsDto>schemasState.snapshot.schemas.at(1);
expect(schema_1.scriptQuery).toEqual('query');
expect(schema_1.scriptCreate).toEqual('create');
expect(schema_1.scriptUpdate).toEqual('update');
expect(schema_1.scriptDelete).toEqual('delete');
expect(schema_1.scriptChange).toEqual('change');
expect(schema_1.previewUrls).toEqual(request);
expectToBeModified(schema_1);
});
it('should add schema to snapshot when created', () => {
const request = new CreateSchemaDto('newName');
const result = new SchemaDetailsDto('id4', 'newName', '', {}, false, false, modified, modifier, modified, modifier, version, [], {});
const result = new SchemaDetailsDto('id4', 'newName', '', {}, false, false, modified, modifier, modified, modifier, version);
schemasService.setup(x => x.postSchema(app, request, modifier, modified))
.returns(() => of(result));

13
src/Squidex/app/shared/state/schemas.state.ts

@ -34,8 +34,7 @@ import {
SchemasService,
UpdateFieldDto,
UpdateSchemaCategoryDto,
UpdateSchemaDto,
UpdateSchemaScriptsDto
UpdateSchemaDto
} from './../services/schemas.service';
import { FieldPropertiesDto } from './../services/schemas.types';
@ -195,7 +194,7 @@ export class SchemasState extends State<Snapshot> {
notify(this.dialogs));
}
public configurePreviewUrls(schema: SchemaDetailsDto, request: { [name: string]: string }, now?: DateTime): Observable<any> {
public configurePreviewUrls(schema: SchemaDetailsDto, request: {}, now?: DateTime): Observable<any> {
return this.schemasService.putPreviewUrls(this.appName, schema.name, request, schema.version).pipe(
tap(dto => {
this.replaceSchema(configurePreviewUrls(schema, request, this.user, dto.version, now));
@ -203,7 +202,7 @@ export class SchemasState extends State<Snapshot> {
notify(this.dialogs));
}
public configureScripts(schema: SchemaDetailsDto, request: UpdateSchemaScriptsDto, now?: DateTime): Observable<any> {
public configureScripts(schema: SchemaDetailsDto, request: {}, now?: DateTime): Observable<any> {
return this.schemasService.putScripts(this.appName, schema.name, request, schema.version).pipe(
tap(dto => {
this.replaceSchema(configureScripts(schema, request, this.user, dto.version, now));
@ -390,7 +389,7 @@ const changeCategory = <T extends SchemaDto>(schema: T, category: string, user:
version
});
const configurePreviewUrls = (schema: SchemaDetailsDto, previewUrls: { [name: string]: string }, user: string, version: Version, now?: DateTime) =>
const configurePreviewUrls = (schema: SchemaDetailsDto, previewUrls: {}, user: string, version: Version, now?: DateTime) =>
schema.with({
previewUrls,
lastModified: now || DateTime.now(),
@ -398,9 +397,9 @@ const configurePreviewUrls = (schema: SchemaDetailsDto, previewUrls: { [name: st
version
});
const configureScripts = (schema: SchemaDetailsDto, scripts: UpdateSchemaScriptsDto, user: string, version: Version, now?: DateTime) =>
const configureScripts = (schema: SchemaDetailsDto, scripts: {}, user: string, version: Version, now?: DateTime) =>
schema.with({
...scripts,
scripts,
lastModified: now || DateTime.now(),
lastModifiedBy: user,
version

67
tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs

@ -1,67 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Core.Model.Schemas
{
public class FieldRegistryTests
{
private readonly FieldRegistry sut = new FieldRegistry(new TypeNameRegistry());
private sealed class InvalidProperties : FieldProperties
{
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{
return default;
}
public override T Accept<T>(IFieldVisitor<T> visitor, IField field)
{
return default;
}
public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null)
{
return null;
}
public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null)
{
return null;
}
}
[Fact]
public void Should_throw_exception_if_creating_field_and_field_is_not_registered()
{
Assert.Throws<InvalidOperationException>(() => sut.CreateRootField(1, "name", Partitioning.Invariant, new InvalidProperties()));
}
[Theory]
[InlineData(typeof(AssetsFieldProperties))]
[InlineData(typeof(BooleanFieldProperties))]
[InlineData(typeof(DateTimeFieldProperties))]
[InlineData(typeof(GeolocationFieldProperties))]
[InlineData(typeof(JsonFieldProperties))]
[InlineData(typeof(NumberFieldProperties))]
[InlineData(typeof(ReferencesFieldProperties))]
[InlineData(typeof(StringFieldProperties))]
[InlineData(typeof(TagsFieldProperties))]
public void Should_create_field_by_properties(Type propertyType)
{
var properties = (FieldProperties)Activator.CreateInstance(propertyType);
var field = sut.CreateRootField(1, "name", Partitioning.Invariant, properties);
Assert.Equal(properties, field.RawProperties);
}
}
}

51
tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs

@ -275,10 +275,59 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
Assert.Throws<ArgumentException>(() => schema_2.ReorderFields(new List<long> { 1, 4 }));
}
[Fact]
public void Should_change_category()
{
var schema_1 = schema_0.ChangeCategory("Category");
Assert.Equal("Category", schema_1.Category);
}
[Fact]
public void Should_configure_scripts()
{
var scripts = new SchemaScripts
{
Query = "<query-script>"
};
var schema_1 = schema_0.ConfigureScripts(scripts);
Assert.Equal(scripts, schema_1.Scripts);
Assert.Equal("<query-script>", schema_1.Scripts.Query);
}
[Fact]
public void Should_configure_preview_urls()
{
var urls = new Dictionary<string, string>
{
["web"] = "Url"
};
var schema_1 = schema_0.ConfigurePreviewUrls(urls);
Assert.Equal(urls, schema_1.PreviewUrls);
Assert.Equal("Url", schema_1.PreviewUrls["web"]);
}
[Fact]
public void Should_serialize_and_deserialize_schema()
{
var schemaSource = TestUtils.MixedSchema();
var schemaSource =
TestUtils.MixedSchema(true)
.ChangeCategory("Category")
.ConfigurePreviewUrls(new Dictionary<string, string>
{
["web"] = "Url"
})
.ConfigureScripts(new SchemaScripts
{
Create = "<create-script>"
});
var schemaTarget = schemaSource.SerializeAndDeserialize();
schemaTarget.Should().BeEquivalentTo(schemaSource);

51
tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/AssertHelper.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using FluentAssertions.Equivalency;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Orleans;
namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
{
public static class AssertHelper
{
public static void ShouldHaveSameEvents(this IEnumerable<IEvent> events, params IEvent[] others)
{
var source = events.ToArray();
source.Should().HaveSameCount(others);
for (var i = 0; i < source.Length; i++)
{
var lhs = source[i];
var rhs = others[i];
lhs.ShouldBeSameEvent(rhs);
}
}
public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs)
{
lhs.Should().BeOfType(rhs.GetType());
((object)lhs).Should().BeEquivalentTo(rhs, o => o.IncludingAllRuntimeProperties().Excluding((IMemberInfo x) => x.SelectedMemberPath == "Properties.IsFrozen"));
}
public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs)
{
lhs.Should().BeOfType(rhs.GetType());
}
public static void ShouldBeEquivalent<T>(this J<T> lhs, T rhs)
{
lhs.Value.Should().BeEquivalentTo(rhs, o => o.IncludingProperties());
}
}
}

526
tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs

@ -0,0 +1,526 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.EventSynchronization;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
{
public class SchemaSynchronizerTests
{
private readonly Func<long> idGenerator;
private readonly IJsonSerializer jsonSerializer = TestUtils.DefaultSerializer;
private readonly NamedId<long> stringId = NamedId.Of(13L, "my-value");
private readonly NamedId<long> nestedId = NamedId.Of(141L, "my-value");
private readonly NamedId<long> arrayId = NamedId.Of(14L, "11-array");
private int fields = 50;
public SchemaSynchronizerTests()
{
idGenerator = () => fields++;
}
[Fact]
public void Should_create_events_if_schema_deleted()
{
var sourceSchema = new Schema("source");
var targetSchema = (Schema)null;
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaDeleted()
);
}
[Fact]
public void Should_create_events_if_category_changed()
{
var sourceSchema = new Schema("source");
var targetSchema = new Schema("target").ChangeCategory("Category");
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaCategoryChanged { Name = "Category" }
);
}
[Fact]
public void Should_create_events_if_scripts_configured()
{
var scripts = new SchemaScripts
{
Create = "<create-script>"
};
var sourceSchema = new Schema("source");
var targetSchema = new Schema("target").ConfigureScripts(scripts);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaScriptsConfigured { Scripts = scripts }
);
}
[Fact]
public void Should_create_events_if_preview_urls_configured()
{
var previewUrls = new Dictionary<string, string>
{
["web"] = "Url"
};
var sourceSchema = new Schema("source");
var targetSchema = new Schema("target").ConfigurePreviewUrls(previewUrls);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaPreviewUrlsConfigured { PreviewUrls = previewUrls }
);
}
[Fact]
public void Should_create_events_if_schema_published()
{
var sourceSchema = new Schema("source");
var targetSchema = new Schema("target").Publish();
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaPublished()
);
}
[Fact]
public void Should_create_events_if_schema_unpublished()
{
var sourceSchema = new Schema("source").Publish();
var targetSchema = new Schema("target");
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaUnpublished()
);
}
[Fact]
public void Should_create_events_if_nested_field_deleted()
{
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name));
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldDeleted { FieldId = nestedId, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_field_deleted()
{
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var targetSchema =
new Schema("target");
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldDeleted { FieldId = stringId }
);
}
[Fact]
public void Should_create_events_if_nested_field_updated()
{
var properties = new StringFieldProperties { IsRequired = true };
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name));
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name, properties));
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldUpdated { Properties = properties, FieldId = nestedId, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_field_updated()
{
var properties = new StringFieldProperties { IsRequired = true };
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var targetSchema =
new Schema("target")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant, properties);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldUpdated { Properties = properties, FieldId = stringId }
);
}
[Fact]
public void Should_create_events_if_nested_field_locked()
{
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name));
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name)).LockField(nestedId.Id, arrayId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldLocked { FieldId = nestedId, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_field_locked()
{
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var targetSchema =
new Schema("target")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant).LockField(stringId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldLocked { FieldId = stringId }
);
}
[Fact]
public void Should_create_events_if_nested_field_hidden()
{
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name));
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name)).HideField(nestedId.Id, arrayId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldHidden { FieldId = nestedId, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_field_hidden()
{
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var targetSchema =
new Schema("target")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant).HideField(stringId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldHidden { FieldId = stringId }
);
}
[Fact]
public void Should_create_events_if_nested_field_shown()
{
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name)).HideField(nestedId.Id, arrayId.Id);
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name));
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldShown { FieldId = nestedId, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_field_shown()
{
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant).HideField(stringId.Id);
var targetSchema =
new Schema("target")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldShown { FieldId = stringId }
);
}
[Fact]
public void Should_create_events_if_nested_field_disabled()
{
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name));
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name)).DisableField(nestedId.Id, arrayId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldDisabled { FieldId = nestedId, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_field_disabled()
{
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var targetSchema =
new Schema("target")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant).DisableField(stringId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldDisabled { FieldId = stringId }
);
}
[Fact]
public void Should_create_events_if_nested_field_enabled()
{
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name)).DisableField(nestedId.Id, arrayId.Id);
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name));
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldEnabled { FieldId = nestedId, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_field_enabled()
{
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant).DisableField(stringId.Id);
var targetSchema =
new Schema("target")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldEnabled { FieldId = stringId }
);
}
[Fact]
public void Should_create_events_if_field_created()
{
var sourceSchema =
new Schema("source");
var targetSchema =
new Schema("target")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant).HideField(stringId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
var createdId = NamedId.Of(50L, stringId.Name);
events.ShouldHaveSameEvents(
new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() },
new FieldHidden { FieldId = createdId }
);
}
[Fact]
public void Should_create_events_if_field_recreated()
{
var sourceSchema =
new Schema("source")
.AddString(stringId.Id, stringId.Name, Partitioning.Invariant);
var targetSchema =
new Schema("target")
.AddTags(stringId.Id, stringId.Name, Partitioning.Invariant);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
var createdId = NamedId.Of(50L, stringId.Name);
events.ShouldHaveSameEvents(
new FieldDeleted { FieldId = stringId },
new FieldAdded { FieldId = createdId, Name = stringId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new TagsFieldProperties() }
);
}
[Fact]
public void Should_create_events_if_nested_field_created()
{
var sourceSchema =
new Schema("source");
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(nestedId.Id, nestedId.Name)).HideField(nestedId.Id, arrayId.Id);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
var id1 = NamedId.Of(50L, arrayId.Name);
var id2 = NamedId.Of(51L, stringId.Name);
events.ShouldHaveSameEvents(
new FieldAdded { FieldId = id1, Name = arrayId.Name, Partitioning = Partitioning.Invariant.Key, Properties = new ArrayFieldProperties() },
new FieldAdded { FieldId = id2, Name = stringId.Name, ParentFieldId = id1, Properties = new StringFieldProperties() },
new FieldHidden { FieldId = id2, ParentFieldId = id1 }
);
}
[Fact]
public void Should_create_events_if_nested_fields_reordered()
{
var id1 = NamedId.Of(1, "f1");
var id2 = NamedId.Of(2, "f1");
var sourceSchema =
new Schema("source")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(10, "f1")
.AddString(11, "f2"));
var targetSchema =
new Schema("target")
.AddArray(arrayId.Id, arrayId.Name, Partitioning.Invariant, f => f
.AddString(20, "f2")
.AddString(15, "f1"));
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaFieldsReordered { FieldIds = new List<long> { 11, 10 }, ParentFieldId = arrayId }
);
}
[Fact]
public void Should_create_events_if_fields_reordered()
{
var id1 = NamedId.Of(1, "f1");
var id2 = NamedId.Of(2, "f1");
var sourceSchema =
new Schema("source")
.AddString(10, "f1", Partitioning.Invariant)
.AddString(11, "f2", Partitioning.Invariant);
var targetSchema =
new Schema("target")
.AddString(20, "f2", Partitioning.Invariant)
.AddString(15, "f1", Partitioning.Invariant);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new SchemaFieldsReordered { FieldIds = new List<long> { 11, 10 } }
);
}
[Fact]
public void Should_create_events_if_fields_reordered_after_sync()
{
var id1 = NamedId.Of(1, "f1");
var id2 = NamedId.Of(2, "f1");
var sourceSchema =
new Schema("source")
.AddString(10, "f1", Partitioning.Invariant)
.AddString(11, "f2", Partitioning.Invariant);
var targetSchema =
new Schema("target")
.AddString(25, "f3", Partitioning.Invariant)
.AddString(20, "f1", Partitioning.Invariant);
var events = sourceSchema.Synchronize(targetSchema, jsonSerializer, idGenerator);
events.ShouldHaveSameEvents(
new FieldDeleted { FieldId = NamedId.Of(11L, "f2") },
new FieldAdded { FieldId = NamedId.Of(50L, "f3"), Name = "f3", Partitioning = Partitioning.Invariant.Key, Properties = new StringFieldProperties() },
new SchemaFieldsReordered { FieldIds = new List<long> { 50, 10 } }
);
}
}
}

4
tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -12,8 +12,8 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="4.9.2" />
<PackageReference Include="FluentAssertions" Version="5.5.3" />
<PackageReference Include="FakeItEasy" Version="5.0.1" />
<PackageReference Include="FluentAssertions" Version="5.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />

4
tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs

@ -59,9 +59,9 @@ namespace Squidex.Domain.Apps.Core
return new NewtonsoftJsonSerializer(serializerSettings);
}
public static Schema MixedSchema()
public static Schema MixedSchema(bool isSingleton = false)
{
var schema = new Schema("user")
var schema = new Schema("user", isSingleton: isSingleton)
.Publish()
.AddArray(101, "root-array", Partitioning.Language, f => f
.AddAssets(201, "nested-assets")

69
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Xunit;
@ -19,10 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
private readonly IStore<string> store = A.Fake<IStore<string>>();
private readonly IPersistence<AppsByNameIndexGrain.GrainState> persistence = A.Fake<IPersistence<AppsByNameIndexGrain.GrainState>>();
private readonly Guid appId1 = Guid.NewGuid();
private readonly Guid appId2 = Guid.NewGuid();
private readonly string appName1 = "my-app1";
private readonly string appName2 = "my-app2";
private readonly NamedId<Guid> appId1 = NamedId.Of(Guid.NewGuid(), "my-app1");
private readonly NamedId<Guid> appId2 = NamedId.Of(Guid.NewGuid(), "my-app2");
private readonly AppsByNameIndexGrain sut;
public AppsByNameIndexGrainTests()
@ -37,11 +36,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
[Fact]
public async Task Should_add_app_id_to_index()
{
await sut.AddAppAsync(appId1, appName1);
await sut.AddAppAsync(appId1.Id, appId1.Name);
var result = await sut.GetAppIdAsync(appName1);
var result = await sut.GetAppIdAsync(appId1.Name);
Assert.Equal(appId1, result);
Assert.Equal(appId1.Id, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<AppsByNameIndexGrain.GrainState>.Ignored))
.MustHaveHappened();
@ -50,79 +49,79 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
[Fact]
public async Task Should_not_be_able_to_reserve_index_if_name_taken()
{
await sut.AddAppAsync(appId2, appName1);
await sut.AddAppAsync(appId2.Id, appId1.Name);
Assert.False(await sut.ReserveAppAsync(appId1, appName1));
Assert.False(await sut.ReserveAppAsync(appId1.Id, appId1.Name));
}
[Fact]
public async Task Should_not_be_able_to_reserve_if_name_reserved()
{
await sut.ReserveAppAsync(appId2, appName1);
await sut.ReserveAppAsync(appId2.Id, appId1.Name);
Assert.False(await sut.ReserveAppAsync(appId1, appName1));
Assert.False(await sut.ReserveAppAsync(appId1.Id, appId1.Name));
}
[Fact]
public async Task Should_not_be_able_to_reserve_if_id_taken()
{
await sut.AddAppAsync(appId1, appName1);
await sut.AddAppAsync(appId1.Id, appId1.Name);
Assert.False(await sut.ReserveAppAsync(appId1, appName2));
Assert.False(await sut.ReserveAppAsync(appId1.Id, appId2.Name));
}
[Fact]
public async Task Should_not_be_able_to_reserve_if_id_reserved()
{
await sut.ReserveAppAsync(appId1, appName1);
await sut.ReserveAppAsync(appId1.Id, appId1.Name);
Assert.False(await sut.ReserveAppAsync(appId1, appName2));
Assert.False(await sut.ReserveAppAsync(appId1.Id, appId2.Name));
}
[Fact]
public async Task Should_be_able_to_reserve_if_id_and_name_not_reserved()
{
await sut.ReserveAppAsync(appId1, appName1);
await sut.ReserveAppAsync(appId1.Id, appId1.Name);
Assert.True(await sut.ReserveAppAsync(appId2, appName2));
Assert.True(await sut.ReserveAppAsync(appId2.Id, appId2.Name));
}
[Fact]
public async Task Should_be_able_to_reserve_after_app_removed()
{
await sut.AddAppAsync(appId1, appName1);
await sut.RemoveAppAsync(appId1);
await sut.AddAppAsync(appId1.Id, appId1.Name);
await sut.RemoveAppAsync(appId1.Id);
Assert.True(await sut.ReserveAppAsync(appId1, appName1));
Assert.True(await sut.ReserveAppAsync(appId1.Id, appId1.Name));
}
[Fact]
public async Task Should_be_able_to_reserve_after_reservation_removed()
{
await sut.ReserveAppAsync(appId1, appName1);
await sut.RemoveReservationAsync(appId1, appName1);
await sut.ReserveAppAsync(appId1.Id, appId1.Name);
await sut.RemoveReservationAsync(appId1.Id, appId1.Name);
Assert.True(await sut.ReserveAppAsync(appId1, appName1));
Assert.True(await sut.ReserveAppAsync(appId1.Id, appId1.Name));
}
[Fact]
public async Task Should_return_many_app_ids()
{
await sut.AddAppAsync(appId1, appName1);
await sut.AddAppAsync(appId2, appName2);
await sut.AddAppAsync(appId1.Id, appId1.Name);
await sut.AddAppAsync(appId2.Id, appId2.Name);
var ids = await sut.GetAppIdsAsync(appName1, appName2);
var ids = await sut.GetAppIdsAsync(appId1.Name, appId2.Name);
Assert.Equal(new List<Guid> { appId1, appId2 }, ids);
Assert.Equal(new List<Guid> { appId1.Id, appId2.Id }, ids);
}
[Fact]
public async Task Should_remove_app_id_from_index()
{
await sut.AddAppAsync(appId1, appName1);
await sut.RemoveAppAsync(appId1);
await sut.AddAppAsync(appId1.Id, appId1.Name);
await sut.RemoveAppAsync(appId1.Id);
var result = await sut.GetAppIdAsync(appName1);
var result = await sut.GetAppIdAsync(appId1.Name);
Assert.Equal(Guid.Empty, result);
@ -135,16 +134,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
var state = new Dictionary<string, Guid>
{
[appName1] = appId1,
[appName2] = appId2
[appId1.Name] = appId1.Id,
[appId2.Name] = appId2.Id
};
await sut.RebuildAsync(state);
Assert.Equal(appId1, await sut.GetAppIdAsync(appName1));
Assert.Equal(appId2, await sut.GetAppIdAsync(appName2));
Assert.Equal(appId1.Id, await sut.GetAppIdAsync(appId1.Name));
Assert.Equal(appId2.Id, await sut.GetAppIdAsync(appId2.Name));
Assert.Equal(new List<Guid> { appId1, appId2 }, await sut.GetAppIdsAsync());
Assert.Equal(new List<Guid> { appId1.Id, appId2.Id }, await sut.GetAppIdsAsync());
Assert.Equal(2, await sut.CountAsync());

3
tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Xunit;
@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
var schema = A.Fake<ISchemaEntity>();
A.CallTo(() => schema.Name).Returns(name);
A.CallTo(() => schema.SchemaDef).Returns(new Schema(name));
return schema;
}

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

Loading…
Cancel
Save