Browse Source

Merge pull request #104 from Squidex/scripting

Scripting
pull/107/head
Sebastian Stehle 9 years ago
committed by GitHub
parent
commit
bc1859a602
  1. BIN
      Jurassic.4.0.0.nupkg
  2. 2
      Squidex.sln.DotSettings
  3. 1
      nuget.config
  4. BIN
      nuget.exe
  5. BIN
      packages/jurassic/4.0.0/jurassic.4.0.0.nupkg
  6. 1
      packages/jurassic/4.0.0/jurassic.4.0.0.nupkg.sha512
  7. 16
      packages/jurassic/4.0.0/jurassic.nuspec
  8. 2
      src/Squidex.Domain.Apps.Core/ContentEnricher.cs
  9. 2
      src/Squidex.Domain.Apps.Core/ContentValidator.cs
  10. 14
      src/Squidex.Domain.Apps.Core/Contents/ContentData.cs
  11. 6
      src/Squidex.Domain.Apps.Core/Contents/ContentFieldData.cs
  12. 7
      src/Squidex.Domain.Apps.Core/Contents/IdContentData.cs
  13. 14
      src/Squidex.Domain.Apps.Core/Contents/NamedContentData.cs
  14. 2
      src/Squidex.Domain.Apps.Core/FieldExtensions.cs
  15. 4
      src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs
  16. 2
      src/Squidex.Domain.Apps.Core/Schemas/Schema.cs
  17. 4
      src/Squidex.Domain.Apps.Core/Schemas/Validators/AssetsValidator.cs
  18. 4
      src/Squidex.Domain.Apps.Core/Schemas/Validators/ReferencesValidator.cs
  19. 132
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataObject.cs
  20. 67
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataProperty.cs
  21. 142
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldObject.cs
  22. 58
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldProperty.cs
  23. 146
      src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/JsonMapper.cs
  24. 21
      src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs
  25. 167
      src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs
  26. 50
      src/Squidex.Domain.Apps.Core/Scripting/JintUser.cs
  27. 25
      src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs
  28. 4
      src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj
  29. 28
      src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs
  30. 4
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs
  31. 2
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs
  32. 22
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs
  33. 5
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  34. 2
      src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj
  35. 2
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs
  36. 12
      src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs
  37. 5
      src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  38. 2
      src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs
  39. 2
      src/Squidex.Domain.Apps.Write/Apps/AppContributors.cs
  40. 3
      src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs
  41. 117
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs
  42. 24
      src/Squidex.Domain.Apps.Write/Contents/ContentDataChangedResult.cs
  43. 11
      src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs
  44. 25
      src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs
  45. 5
      src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs
  46. 13
      src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs
  47. 36
      src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs
  48. 1
      src/Squidex.Domain.Users/UserExtensions.cs
  49. 2
      src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs
  50. 2
      src/Squidex.Infrastructure.Redis/RedisSubscription.cs
  51. 4
      src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs
  52. 2
      src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs
  53. 47
      src/Squidex.Infrastructure/CollectionExtensions.cs
  54. 25
      src/Squidex.Infrastructure/DomainForbiddenException.cs
  55. 2
      src/Squidex.Infrastructure/DomainObjectException.cs
  56. 2
      src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs
  57. 2
      src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs
  58. 5
      src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs
  59. 2
      src/Squidex.Infrastructure/Language.cs
  60. 2
      src/Squidex.Infrastructure/PropertiesBag.cs
  61. 6
      src/Squidex.Infrastructure/PropertyValue.cs
  62. 2
      src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs
  63. 2
      src/Squidex.Infrastructure/StringExtensions.cs
  64. 1
      src/Squidex.Infrastructure/TypeNameRegistry.cs
  65. 7
      src/Squidex/Config/Domain/WriteModule.cs
  66. 6
      src/Squidex/Config/Identity/IdentityServices.cs
  67. 1
      src/Squidex/Config/Identity/IdentityUsage.cs
  68. 1
      src/Squidex/Config/Identity/LazyClientStore.cs
  69. 1
      src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs
  70. 4
      src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs
  71. 43
      src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs
  72. 30
      src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs
  73. 24
      src/Squidex/Controllers/Api/Schemas/SchemasController.cs
  74. 69
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  75. 4
      src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs
  76. 2
      src/Squidex/Controllers/ContentApi/Models/ContentDto.cs
  77. 13
      src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs
  78. 4
      src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs
  79. 2
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
  80. 2
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs
  81. 2
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs
  82. 2
      src/Squidex/Pipeline/Swagger/SwaggerHelper.cs
  83. 2
      src/Squidex/Squidex.csproj
  84. 5
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  85. 1
      src/Squidex/app/features/schemas/declarations.ts
  86. 2
      src/Squidex/app/features/schemas/module.ts
  87. 2
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  88. 12
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  89. 9
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  90. 33
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.html
  91. 14
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss
  92. 123
      src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.ts
  93. 6
      src/Squidex/app/framework/angular/dialog-renderer.component.html
  94. 4
      src/Squidex/app/framework/angular/dialog-renderer.component.scss
  95. 1
      src/Squidex/app/framework/angular/jscript-editor.component.html
  96. 8
      src/Squidex/app/framework/angular/jscript-editor.component.scss
  97. 108
      src/Squidex/app/framework/angular/jscript-editor.component.ts
  98. 2
      src/Squidex/app/framework/angular/json-editor.component.scss
  99. 1
      src/Squidex/app/framework/declarations.ts
  100. 3
      src/Squidex/app/framework/module.ts

BIN
Jurassic.4.0.0.nupkg

Binary file not shown.

2
Squidex.sln.DotSettings

@ -24,8 +24,10 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToAutoProperty/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToAutoPropertyWhenPossible/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToConstant_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InheritdocConsiderUsage/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBePrivate_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=Mvc_002EViewNotResolved/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterOnlyUsedForPreconditionCheck_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ReturnTypeCanBeEnumerable_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnnecessaryWhitespace/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedAutoPropertyAccessor_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>

1
nuget.config

@ -2,5 +2,6 @@
<configuration>
<packageSources>
<add key="ImageSharp Nightly" value="https://www.myget.org/F/imagesharp/api/v3/index.json" />
<add key="Local" value="packages" />
</packageSources>
</configuration>

BIN
nuget.exe

Binary file not shown.

BIN
packages/jurassic/4.0.0/jurassic.4.0.0.nupkg

Binary file not shown.

1
packages/jurassic/4.0.0/jurassic.4.0.0.nupkg.sha512

@ -0,0 +1 @@
p41pr8YRXXiqrW5DrLpCxbsyTZwZ9ONcdFjYaHorvCuvPeMiBgl+WOFKkzLENEnJdh22tkh7FKjfSMW1S1jtZg==

16
packages/jurassic/4.0.0/jurassic.nuspec

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Jurassic</id>
<version>4.0.0</version>
<authors>Paul Bartrum</authors>
<owners>Paul Bartrum</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A .NET library to parse and execute JavaScript code.</description>
<dependencies>
<group targetFramework=".NETStandard2.0">
<dependency id="System.Reflection.Emit.Lightweight" version="4.3.0" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
</package>

2
src/Squidex.Domain.Apps.Core/ContentEnricher.cs

@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Core
var key = partitionItem.Key;
if (!fieldData.TryGetValue(key, out JToken value) || value == null || value.Type == JTokenType.Null)
if (!fieldData.TryGetValue(key, out var value) || value == null || value.Type == JTokenType.Null)
{
fieldData.AddValue(key, defaultValue);
}

2
src/Squidex.Domain.Apps.Core/ContentValidator.cs

@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Core
{
var fieldName = fieldData.Key;
if (!schema.FieldsByName.TryGetValue(fieldData.Key, out Field field))
if (!schema.FieldsByName.TryGetValue(fieldData.Key, out var field))
{
errors.AddError("<FIELD> is not a known field", fieldName);
}

14
src/Squidex.Domain.Apps.Core/Contents/ContentData.cs

@ -9,9 +9,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
// ReSharper disable InvertIf
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
var resultValue = new ContentFieldData();
foreach (var partitionValue in fieldValue.Value.Where(x => IsNotNull(x.Value)))
foreach (var partitionValue in fieldValue.Value.Where(x => !x.Value.IsNull()))
{
resultValue[partitionValue.Key] = partitionValue.Value;
}
@ -120,16 +120,6 @@ namespace Squidex.Domain.Apps.Core.Contents
return this.DictionaryHashCode();
}
protected static bool IsNull(JToken value)
{
return value == null || value.Type == JTokenType.Null;
}
protected static bool IsNotNull(JToken value)
{
return value != null && value.Type != JTokenType.Null;
}
public abstract T GetKey(Field field);
}
}

6
src/Squidex.Domain.Apps.Core/Contents/ContentFieldData.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class ContentFieldData : Dictionary<string, JToken>, IEquatable<ContentFieldData>
{
private static readonly JTokenEqualityComparer JTokenEqualityComparer = new JTokenEqualityComparer();
public ContentFieldData()
: base(StringComparer.OrdinalIgnoreCase)
{
@ -43,12 +45,12 @@ namespace Squidex.Domain.Apps.Core.Contents
public bool Equals(ContentFieldData other)
{
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other));
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other, EqualityComparer<string>.Default, JTokenEqualityComparer));
}
public override int GetHashCode()
{
return this.DictionaryHashCode();
return this.DictionaryHashCode(EqualityComparer<string>.Default, JTokenEqualityComparer);
}
}
}

7
src/Squidex.Domain.Apps.Core/Contents/IdContentData.cs

@ -13,6 +13,7 @@ using System.Text;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
// ReSharper disable InvertIf
@ -65,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Contents
continue;
}
foreach (var partitionValue in fieldData.Where(x => IsNotNull(x.Value)).ToList())
foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull()).ToList())
{
var newValue = referenceField.RemoveDeletedReferences(partitionValue.Value, deletedReferencedIds);
@ -85,7 +86,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var fieldValue in this)
{
if (!schema.FieldsById.TryGetValue(fieldValue.Key, out Field field))
if (!schema.FieldsById.TryGetValue(fieldValue.Key, out var field))
{
continue;
}
@ -96,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var partitionValue in fieldValue.Value)
{
if (IsNull(partitionValue.Value))
if (partitionValue.Value.IsNull())
{
encodedValue[partitionValue.Key] = null;
}

14
src/Squidex.Domain.Apps.Core/Contents/NamedContentData.cs

@ -13,7 +13,9 @@ using System.Text;
using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json;
// ReSharper disable SuspiciousTypeConversion.Global
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Core.Contents
@ -52,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var fieldValue in this)
{
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out Field field))
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field))
{
continue;
}
@ -65,7 +67,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var partitionValue in fieldValue.Value)
{
if (IsNull(partitionValue.Value))
if (partitionValue.Value.IsNull())
{
encodedValue[partitionValue.Key] = null;
}
@ -100,7 +102,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var fieldValue in this)
{
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out Field field) || (excludeHidden && field.IsHidden))
if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field) || (excludeHidden && field.IsHidden))
{
continue;
}
@ -114,7 +116,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
var languageCode = languageConfig.Key;
if (fieldValues.TryGetValue(languageCode, out JToken value))
if (fieldValues.TryGetValue(languageCode, out var value))
{
fieldResult.Add(languageCode, value);
}
@ -126,7 +128,7 @@ namespace Squidex.Domain.Apps.Core.Contents
}
else
{
if (fieldValues.TryGetValue(codeForInvariant, out JToken value))
if (fieldValues.TryGetValue(codeForInvariant, out var value))
{
fieldResult.Add(codeForInvariant, value);
}
@ -168,7 +170,7 @@ namespace Squidex.Domain.Apps.Core.Contents
foreach (var language in languagePreferences)
{
if (fieldValues.TryGetValue(language, out JToken value) && value != null)
if (fieldValues.TryGetValue(language, out var value) && value != null)
{
result[fieldValue.Key] = value;

2
src/Squidex.Domain.Apps.Core/FieldExtensions.cs

@ -40,8 +40,6 @@ namespace Squidex.Domain.Apps.Core
public static async Task ValidateAsync(this Field field, JToken value, ValidationContext context, Action<string> addError)
{
Guard.NotNull(value, nameof(value));
try
{
var typedValue = value.IsNull() ? null : field.ConvertValue(value);

4
src/Squidex.Domain.Apps.Core/Schemas/Field_Generic.cs

@ -46,9 +46,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
newProperties.Freeze();
var typedProperties = newProperties as T;
if (typedProperties == null)
if (!(newProperties is T typedProperties))
{
throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties));
}

2
src/Squidex.Domain.Apps.Core/Schemas/Schema.cs

@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
Guard.NotNull(updater, nameof(updater));
if (!fieldsById.TryGetValue(fieldId, out Field field))
if (!fieldsById.TryGetValue(fieldId, out var field))
{
throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Field));
}

4
src/Squidex.Domain.Apps.Core/Schemas/Validators/AssetsValidator.cs

@ -26,9 +26,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Validators
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
{
var assets = value as AssetsValue;
if (assets == null || assets.AssetIds.Count == 0)
if (!(value is AssetsValue assets) || assets.AssetIds.Count == 0)
{
if (isRequired && !context.IsOptional)
{

4
src/Squidex.Domain.Apps.Core/Schemas/Validators/ReferencesValidator.cs

@ -28,9 +28,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Validators
public async Task ValidateAsync(object value, ValidationContext context, Action<string> addError)
{
var references = value as ReferencesValue;
if (references == null || references.ContentIds.Count == 0)
if (!(value is ReferencesValue references) || references.ContentIds.Count == 0)
{
if (isRequired && !context.IsOptional)
{

132
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataObject.cs

@ -0,0 +1,132 @@
// ==========================================================================
// ContentDataObject.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentDataObject : ObjectInstance
{
private readonly NamedContentData contentData;
private HashSet<string> fieldsToDelete;
private Dictionary<string, ContentDataProperty> fieldProperties;
private bool isChanged;
public ContentDataObject(Engine engine, NamedContentData contentData)
: base(engine)
{
Extensible = true;
this.contentData = contentData;
}
public void MarkChanged()
{
isChanged = true;
}
public bool TryUpdate(out NamedContentData result)
{
result = contentData;
if (isChanged)
{
if (fieldsToDelete != null)
{
foreach (var field in fieldsToDelete)
{
contentData.Remove(field);
}
}
if (fieldProperties != null)
{
foreach (var kvp in fieldProperties)
{
if (kvp.Value.ContentField.TryUpdate(out var fieldData))
{
contentData[kvp.Key] = fieldData;
}
}
}
}
return isChanged;
}
public override void RemoveOwnProperty(string propertyName)
{
if (fieldsToDelete == null)
{
fieldsToDelete = new HashSet<string>();
}
fieldsToDelete.Add(propertyName);
fieldProperties?.Remove(propertyName);
MarkChanged();
}
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError)
{
EnsurePropertiesInitialized();
if (!fieldProperties.ContainsKey(propertyName))
{
fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value };
}
return true;
}
public override void Put(string propertyName, JsValue value, bool throwOnError)
{
EnsurePropertiesInitialized();
fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this)).Value = value;
}
public override PropertyDescriptor GetOwnProperty(string propertyName)
{
EnsurePropertiesInitialized();
return fieldProperties.GetOrDefault(propertyName) ?? new PropertyDescriptor(new ObjectInstance(Engine) { Extensible = true }, true, false, true);
}
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties()
{
EnsurePropertiesInitialized();
foreach (var property in fieldProperties)
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
}
private void EnsurePropertiesInitialized()
{
if (fieldProperties == null)
{
fieldProperties = new Dictionary<string, ContentDataProperty>(contentData.Count);
foreach (var kvp in contentData)
{
fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false)));
}
}
}
}
}

67
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentDataProperty.cs

@ -0,0 +1,67 @@
// ==========================================================================
// ContentFieldProperty.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentDataProperty : PropertyDescriptor
{
private readonly ContentDataObject contentData;
private ContentFieldObject contentField;
private JsValue value;
public override JsValue Value
{
get { return value; }
set
{
if (!Equals(this.value, value))
{
if (value == null || !value.IsObject())
{
throw new JavaScriptException("Can only assign object to content data.");
}
var obj = value.AsObject();
contentField = new ContentFieldObject(contentData, new ContentFieldData(), true);
foreach (var kvp in obj.GetOwnProperties())
{
contentField.Put(kvp.Key, kvp.Value.Value, true);
}
this.value = new JsValue(contentField);
}
}
}
public ContentFieldObject ContentField
{
get { return contentField; }
}
public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null)
: base(null, true, true, true)
{
this.contentData = contentData;
this.contentField = contentField;
if (contentField != null)
{
value = new JsValue(contentField);
}
}
}
}

142
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldObject.cs

@ -0,0 +1,142 @@
// ==========================================================================
// ContentFieldObject.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentFieldObject : ObjectInstance
{
private readonly ContentDataObject contentData;
private readonly ContentFieldData fieldData;
private HashSet<string> valuesToDelete;
private Dictionary<string, ContentFieldProperty> valueProperties;
private bool isChanged;
public bool IsChanged
{
get { return isChanged; }
}
public ContentFieldData FieldData
{
get { return fieldData; }
}
public ContentFieldObject(ContentDataObject contentData, ContentFieldData fieldData, bool isNew)
: base(contentData.Engine)
{
Extensible = true;
this.contentData = contentData;
this.fieldData = fieldData;
if (isNew)
{
MarkChanged();
}
}
public void MarkChanged()
{
isChanged = true;
contentData.MarkChanged();
}
public bool TryUpdate(out ContentFieldData result)
{
result = fieldData;
if (isChanged)
{
if (valuesToDelete != null)
{
foreach (var field in valuesToDelete)
{
fieldData.Remove(field);
}
}
if (valueProperties != null)
{
foreach (var kvp in valueProperties)
{
if (kvp.Value.IsChanged)
{
fieldData[kvp.Key] = kvp.Value.ContentValue;
}
}
}
}
return isChanged;
}
public override void RemoveOwnProperty(string propertyName)
{
if (valuesToDelete == null)
{
valuesToDelete = new HashSet<string>();
}
valuesToDelete.Add(propertyName);
valueProperties?.Remove(propertyName);
MarkChanged();
}
public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError)
{
EnsurePropertiesInitialized();
if (!valueProperties.ContainsKey(propertyName))
{
valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value };
}
return true;
}
public override PropertyDescriptor GetOwnProperty(string propertyName)
{
EnsurePropertiesInitialized();
return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined;
}
public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties()
{
EnsurePropertiesInitialized();
foreach (var property in valueProperties)
{
yield return new KeyValuePair<string, PropertyDescriptor>(property.Key, property.Value);
}
}
private void EnsurePropertiesInitialized()
{
if (valueProperties == null)
{
valueProperties = new Dictionary<string, ContentFieldProperty>(FieldData.Count);
foreach (var kvp in FieldData)
{
valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value));
}
}
}
}
}

58
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/ContentFieldProperty.cs

@ -0,0 +1,58 @@
// ==========================================================================
// ContentFieldProperty.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Jint.Native;
using Jint.Runtime.Descriptors;
using Newtonsoft.Json.Linq;
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public sealed class ContentFieldProperty : PropertyDescriptor
{
private readonly ContentFieldObject contentField;
private JToken contentValue;
private JsValue value;
private bool isChanged;
public override JsValue Value
{
get { return value ?? (value = JsonMapper.Map(contentValue, contentField.Engine)); }
set
{
if (!Equals(this.value, value))
{
this.value = value;
contentValue = null;
contentField.MarkChanged();
isChanged = true;
}
}
}
public JToken ContentValue
{
get { return contentValue ?? (contentValue = JsonMapper.Map(value)); }
}
public bool IsChanged
{
get { return isChanged; }
}
public ContentFieldProperty(ContentFieldObject contentField, JToken contentValue = null)
: base(null, true, true, true)
{
this.contentField = contentField;
this.contentValue = contentValue;
}
}
}

146
src/Squidex.Domain.Apps.Core/Scripting/ContentWrapper/JsonMapper.cs

@ -0,0 +1,146 @@
// ==========================================================================
// JsonMapper.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Newtonsoft.Json.Linq;
// ReSharper disable SwitchStatementMissingSomeCases
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
public static class JsonMapper
{
public static JsValue Map(JToken value, Engine engine)
{
if (value == null)
{
return JsValue.Null;
}
switch (value.Type)
{
case JTokenType.Date:
case JTokenType.Guid:
case JTokenType.String:
case JTokenType.Uri:
case JTokenType.TimeSpan:
return new JsValue((string)value);
case JTokenType.Null:
return JsValue.Null;
case JTokenType.Undefined:
return JsValue.Undefined;
case JTokenType.Integer:
return new JsValue((long)value);
case JTokenType.Float:
return new JsValue((double)value);
case JTokenType.Boolean:
return new JsValue((bool)value);
case JTokenType.Object:
{
var obj = (JObject)value;
var target = new ObjectInstance(engine);
foreach (var property in obj)
{
target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true);
}
return target;
}
case JTokenType.Array:
{
var arr = (JArray)value;
var target = new JsValue[arr.Count];
for (var i = 0; i < arr.Count; i++)
{
target[i] = Map(arr[i], engine);
}
return engine.Array.Construct(target);
}
}
throw new ArgumentException("Invalid json type", nameof(value));
}
public static JToken Map(JsValue value)
{
if (value == null || value.IsNull())
{
return JValue.CreateNull();
}
if (value.IsUndefined())
{
return JValue.CreateUndefined();
}
if (value.IsString())
{
return new JValue(value.AsString());
}
if (value.IsBoolean())
{
return new JValue(value.AsBoolean());
}
if (value.IsNumber())
{
return new JValue(value.AsNumber());
}
if (value.IsDate())
{
return new JValue(value.AsDate().ToDateTime());
}
if (value.IsRegExp())
{
return JValue.CreateString(value.AsRegExp().Value?.ToString());
}
if (value.IsArray())
{
var arr = value.AsArray();
var target = new JArray();
for (var i = 0; i < arr.GetLength(); i++)
{
target.Add(Map(arr.Get(i.ToString())));
}
return target;
}
if (value.IsObject())
{
var obj = value.AsObject();
var target = new JObject();
foreach (var kvp in obj.GetOwnProperties())
{
target[kvp.Key] = Map(kvp.Value.Value);
}
return target;
}
throw new ArgumentException("Invalid json type", nameof(value));
}
}
}

21
src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs

@ -0,0 +1,21 @@
// ==========================================================================
// IScriptEngine.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public interface IScriptEngine
{
void Execute(ScriptContext context, string script, string operationName);
NamedContentData ExecuteAndTransform(ScriptContext context, string script, string operationName);
NamedContentData Transform(ScriptContext context, string script);
}
}

167
src/Squidex.Domain.Apps.Core/Scripting/JintScriptEngine.cs

@ -0,0 +1,167 @@
// ==========================================================================
// JintScriptEngine.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Jint;
using Jint.Native.Object;
using Jint.Parser;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Infrastructure;
// ReSharper disable InvertIf
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class JintScriptEngine : IScriptEngine
{
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200);
public void Execute(ScriptContext context, string script, string operationName)
{
Guard.NotNull(context, nameof(context));
if (!string.IsNullOrWhiteSpace(script))
{
var engine = CreateScriptEngine(context);
EnableDisallow(engine);
EnableReject(engine, operationName);
Execute(engine, script, operationName);
}
}
public NamedContentData ExecuteAndTransform(ScriptContext context, string script, string operationName)
{
Guard.NotNull(context, nameof(context));
var result = context.Data;
if (!string.IsNullOrWhiteSpace(script))
{
var engine = CreateScriptEngine(context);
EnableDisallow(engine);
EnableReject(engine, operationName);
engine.SetValue("replace", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
data.TryUpdate(out result);
}
}));
Execute(engine, script, operationName);
}
return result;
}
public NamedContentData Transform(ScriptContext context, string script)
{
Guard.NotNull(context, nameof(context));
var result = context.Data;
if (!string.IsNullOrWhiteSpace(script))
{
try
{
var engine = CreateScriptEngine(context);
engine.SetValue("replace", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
data.TryUpdate(out result);
}
}));
engine.Execute(script);
}
catch (Exception)
{
result = context.Data;
}
}
return result;
}
private static void Execute(Engine engine, string script, string operationName)
{
try
{
engine.Execute(script);
}
catch (ParserException ex)
{
throw new ValidationException($"Failed to {operationName} with javascript syntaxs error.", new ValidationError(ex.Message));
}
catch (JavaScriptException ex)
{
throw new ValidationException($"Failed to {operationName} with javascript error.", new ValidationError(ex.Message));
}
}
private Engine CreateScriptEngine(ScriptContext context)
{
var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict());
var contextInstance = new ObjectInstance(engine);
if (context.Data != null)
{
contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true);
}
if (context.OldData != null)
{
contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.OldData), true, true, true);
}
if (context.User != null)
{
contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false);
}
engine.SetValue("ctx", contextInstance);
return engine;
}
private static void EnableDisallow(Engine engine)
{
engine.SetValue("disallow", new Action<string>(message =>
{
var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed";
throw new DomainForbiddenException(exMessage);
}));
}
private static void EnableReject(Engine engine, string operationName)
{
Guard.NotNullOrEmpty(operationName, nameof(operationName));
engine.SetValue("reject", new Action<string>(message =>
{
var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null;
throw new ValidationException($"Script rejected to {operationName}.", errors);
}));
}
}
}

50
src/Squidex.Domain.Apps.Core/Scripting/JintUser.cs

@ -0,0 +1,50 @@
// ==========================================================================
// JintUser.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using System.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class JintUser : ObjectInstance
{
public JintUser(Engine engine, ClaimsPrincipal principal)
: base(engine)
{
var subjectId = principal.OpenIdSubject();
var isClient = string.IsNullOrWhiteSpace(subjectId);
if (!isClient)
{
FastAddProperty("id", subjectId, false, true, false);
FastAddProperty("isClient", false, false, true, false);
}
else
{
FastAddProperty("id", principal.OpenIdClientId(), false, true, false);
FastAddProperty("isClient", true, false, true, false);
}
FastAddProperty("email", principal.OpenIdEmail(), false, true, false);
var claimsInstance = new ObjectInstance(engine);
foreach (var group in principal.Claims.GroupBy(x => x.Type))
{
claimsInstance.FastAddProperty(group.Key, engine.Array.Construct(group.Select(x => new JsValue(x.Value)).ToArray()), false, true, false);
}
FastAddProperty("claims", claimsInstance, false, true, false);
}
}
}

25
src/Squidex.Domain.Apps.Core/Scripting/ScriptContext.cs

@ -0,0 +1,25 @@
// ==========================================================================
// ScriptContext.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptContext
{
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; }
public NamedContentData Data { get; set; }
public NamedContentData OldData { get; set; }
}
}

4
src/Squidex.Domain.Apps.Core/Squidex.Domain.Apps.Core.csproj

@ -10,7 +10,9 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.2.0" />
<PackageReference Include="Jint" Version="2.11.23" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="NJsonSchema" Version="9.5.0" />
<PackageReference Include="NodaTime" Version="2.2.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" />

28
src/Squidex.Domain.Apps.Events/Schemas/ScriptsConfigured.cs

@ -0,0 +1,28 @@
// ==========================================================================
// ScriptsConfigured.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Events.Schemas
{
[TypeName("ScriptsConfiguredEvent")]
public sealed class ScriptsConfigured : SchemaEvent
{
public string ScriptQuery { get; set; }
public string ScriptCreate { get; set; }
public string ScriptUpdate { get; set; }
public string ScriptDelete { get; set; }
public string ScriptPublish { get; set; }
public string ScriptUnpublish { get; set; }
}
}

4
src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs

@ -45,9 +45,9 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
{
var value = Visit(nodeIn.Source);
if (value is DateTimeOffset)
if (value is DateTimeOffset dateTimeOffset)
{
return Instant.FromDateTimeOffset((DateTimeOffset)value);
return Instant.FromDateTimeOffset(dateTimeOffset);
}
return InstantPattern.General.Parse(Visit(nodeIn.Source).ToString()).Value;

2
src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
{
var edmName = propertyNames[1].UnescapeEdmField();
if (!schema.FieldsByName.TryGetValue(edmName, out Field field))
if (!schema.FieldsByName.TryGetValue(edmName, out var field))
{
throw new NotSupportedException();
}

22
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs

@ -25,6 +25,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
[BsonElement]
public string Name { get; set; }
public string ScriptUnpublish { get; set; }
[BsonRequired]
[BsonElement]
public string Schema { get; set; }
@ -53,6 +55,26 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
[BsonElement]
public bool IsDeleted { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string ScriptQuery { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string ScriptCreate { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string ScriptUpdate { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string ScriptDelete { get; set; }
[BsonIgnoreIfNull]
[BsonElement]
public string ScriptPublish { get; set; }
Schema ISchemaEntity.Schema
{
get { return schema.Value; }

5
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs

@ -103,6 +103,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry));
}
protected Task On(ScriptsConfigured @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(@event, headers, e => SimpleMapper.Map(@event, e));
}
protected Task On(SchemaDeleted @event, EnvelopeHeaders headers)
{
return Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true);

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

@ -14,7 +14,7 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Read\Squidex.Domain.Apps.Read.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" />
<PackageReference Include="MongoDB.Driver" Version="2.4.4" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>

2
src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs

@ -14,6 +14,8 @@ using GraphQL.Types;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Infrastructure;
// ReSharper disable ImpureMethodCallOnReadonlyValueField
namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types
{
public sealed class ContentQueryGraphType : ObjectGraphType

12
src/Squidex.Domain.Apps.Read/Schemas/ISchemaEntity.cs

@ -18,6 +18,18 @@ namespace Squidex.Domain.Apps.Read.Schemas
bool IsDeleted { get; }
string ScriptQuery { get; }
string ScriptCreate { get; }
string ScriptUpdate { get; }
string ScriptDelete { get; }
string ScriptPublish { get; }
string ScriptUnpublish { get; }
Schema Schema { get; }
}
}

5
src/Squidex.Domain.Apps.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -17,6 +17,7 @@ using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
// ReSharper disable ConvertIfStatementToSwitchStatement
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf
@ -125,6 +126,10 @@ namespace Squidex.Domain.Apps.Read.Schemas.Services.Implementations
{
Remove(schemaUpdatedEvent.AppId, schemaUpdatedEvent.SchemaId);
}
else if (@event.Payload is ScriptsConfigured scriptsConfiguredEvent)
{
Remove(scriptsConfiguredEvent.AppId, scriptsConfiguredEvent.SchemaId);
}
return TaskHelper.Done;
}

2
src/Squidex.Domain.Apps.Read/Webhooks/WebhookSender.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Read.Webhooks
{
try
{
HttpRequestMessage request = BuildRequest(job);
var request = BuildRequest(job);
HttpResponseMessage response = null;
var responseString = string.Empty;

2
src/Squidex.Domain.Apps.Write/Apps/AppContributors.cs

@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Write.Apps
private void ThrowIfFound(string contributorId, PermissionLevel permission, Func<string> message)
{
if (contributors.TryGetValue(contributorId, out PermissionLevel currentPermission) && currentPermission == permission)
if (contributors.TryGetValue(contributorId, out var currentPermission) && currentPermission == permission)
{
var error = new ValidationError("Contributor is already part of the app with same permissions", "ContributorId");

3
src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs

@ -7,12 +7,15 @@
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Domain.Apps.Write.Contents.Commands
{
public abstract class ContentCommand : SchemaCommand, IAggregateCommand
{
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; }
Guid IAggregateCommand.AggregateId

117
src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs

@ -11,10 +11,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.Contents.Commands;
using Squidex.Infrastructure;
@ -32,66 +36,120 @@ namespace Squidex.Domain.Apps.Write.Contents
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
private readonly ISchemaProvider schemas;
private readonly IScriptEngine scriptEngine;
public ContentCommandMiddleware(
IAggregateHandler handler,
IAppProvider appProvider,
IAssetRepository assetRepository,
ISchemaProvider schemas,
IScriptEngine scriptEngine,
IContentRepository contentRepository)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(schemas, nameof(schemas));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.handler = handler;
this.schemas = schemas;
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
}
protected async Task On(CreateContent command, CommandContext context)
{
await ValidateAsync(command, () => "Failed to create content", true);
await handler.CreateAsync<ContentDomainObject>(context, c =>
await handler.CreateAsync<ContentDomainObject>(context, async content =>
{
c.Create(command);
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, command.Data);
command.Data = scriptEngine.ExecuteAndTransform(scriptContext, schemaAndApp.SchemaEntity.ScriptCreate, "create content");
command.Data.Enrich(schemaAndApp.SchemaEntity.Schema, schemaAndApp.AppEntity.PartitionResolver);
await ValidateAsync(schemaAndApp, command, () => "Failed to create content", false);
context.Complete(EntityCreatedResult.Create(command.Data, c.Version));
content.Create(command);
context.Complete(EntityCreatedResult.Create(command.Data, content.Version));
});
}
protected async Task On(UpdateContent command, CommandContext context)
{
await ValidateAsync(command, () => "Failed to update content");
await handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, command.Data);
command.Data = scriptEngine.ExecuteAndTransform(scriptContext, schemaAndApp.SchemaEntity.ScriptUpdate, "update content");
await handler.UpdateAsync<ContentDomainObject>(context, c => c.Update(command));
await ValidateAsync(schemaAndApp, command, () => "Failed to update content", false);
content.Update(command);
context.Complete(new ContentDataChangedResult(content.Data, content.Version));
});
}
protected async Task On(PatchContent command, CommandContext context)
{
await ValidateAsync(command, () => "Failed to patch content");
await handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command, command.Data);
command.Data = scriptEngine.ExecuteAndTransform(scriptContext, schemaAndApp.SchemaEntity.ScriptUpdate, "patch content");
await ValidateAsync(schemaAndApp, command, () => "Failed to patch content", true);
await handler.UpdateAsync<ContentDomainObject>(context, c => c.Patch(command));
content.Patch(command);
context.Complete(new ContentDataChangedResult(content.Data, content.Version));
});
}
protected Task On(PublishContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, c => c.Publish(command));
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command);
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptPublish, "publish content");
content.Publish(command);
});
}
protected Task On(UnpublishContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, c => c.Unpublish(command));
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command);
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptUnpublish, "unpublish content");
content.Unpublish(command);
});
}
protected Task On(DeleteContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, c => c.Delete(command));
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
var schemaAndApp = await ResolveSchemaAndAppAsync(command);
var scriptContext = CreateScriptContext(content, command);
scriptEngine.Execute(scriptContext, schemaAndApp.SchemaEntity.ScriptDelete, "delete content");
content.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
@ -102,16 +160,10 @@ namespace Squidex.Domain.Apps.Write.Contents
}
}
private async Task ValidateAsync(ContentDataCommand command, Func<string> message, bool enrich = false)
private async Task ValidateAsync((ISchemaEntity Schema, IAppEntity App) schemaAndApp, ContentDataCommand command, Func<string> message, bool partial)
{
Guard.Valid(command, nameof(command), message);
var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id);
var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id);
await Task.WhenAll(taskForApp, taskForSchema);
var schemaObject = taskForSchema.Result.Schema;
var schemaErrors = new List<ValidationError>();
var appId = command.AppId.Id;
@ -127,17 +179,34 @@ namespace Squidex.Domain.Apps.Write.Contents
return assetRepository.QueryNotFoundAsync(appId, assetIds.ToList());
});
await command.Data.ValidateAsync(validationContext, schemaObject, taskForApp.Result.PartitionResolver, schemaErrors);
if (partial)
{
await command.Data.ValidatePartialAsync(validationContext, schemaAndApp.Schema.Schema, schemaAndApp.App.PartitionResolver, schemaErrors);
}
else
{
await command.Data.ValidateAsync(validationContext, schemaAndApp.Schema.Schema, schemaAndApp.App.PartitionResolver, schemaErrors);
}
if (schemaErrors.Count > 0)
{
throw new ValidationException(message(), schemaErrors);
}
}
if (enrich)
{
command.Data.Enrich(schemaObject, taskForApp.Result.PartitionResolver);
}
private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, NamedContentData data = null)
{
return new ScriptContext { ContentId = content.Id, Data = data, OldData = content.Data, User = command.User };
}
private async Task<(ISchemaEntity SchemaEntity, IAppEntity AppEntity)> ResolveSchemaAndAppAsync(SchemaCommand command)
{
var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id);
var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id);
await Task.WhenAll(taskForApp, taskForSchema);
return (taskForSchema.Result, taskForApp.Result);
}
}
}

24
src/Squidex.Domain.Apps.Write/Contents/ContentDataChangedResult.cs

@ -0,0 +1,24 @@
// ==========================================================================
// ContentChangedResult.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Domain.Apps.Write.Contents
{
public sealed class ContentDataChangedResult : EntitySavedResult
{
public NamedContentData Data { get; }
public ContentDataChangedResult(NamedContentData data, long version)
: base(version)
{
Data = data;
}
}
}

11
src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs

@ -35,6 +35,11 @@ namespace Squidex.Domain.Apps.Write.Contents
get { return isPublished; }
}
public NamedContentData Data
{
get { return data; }
}
public ContentDomainObject(Guid id, int version)
: base(id, version)
{
@ -122,7 +127,7 @@ namespace Squidex.Domain.Apps.Write.Contents
VerifyCreatedAndNotDeleted();
if (!command.Data.Equals(data))
if (!command.Data.Equals(Data))
{
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated()));
}
@ -136,9 +141,9 @@ namespace Squidex.Domain.Apps.Write.Contents
VerifyCreatedAndNotDeleted();
var newData = data.MergeInto(command.Data);
var newData = Data.MergeInto(command.Data);
if (!newData.Equals(data))
if (!newData.Equals(Data))
{
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = newData }));
}

25
src/Squidex.Domain.Apps.Write/Schemas/Commands/ConfigureScripts.cs

@ -0,0 +1,25 @@
// ==========================================================================
// ConfigureScripts.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Domain.Apps.Write.Schemas.Commands
{
public sealed class ConfigureScripts : SchemaAggregateCommand
{
public string ScriptQuery { get; set; }
public string ScriptCreate { get; set; }
public string ScriptUpdate { get; set; }
public string ScriptDelete { get; set; }
public string ScriptPublish { get; set; }
public string ScriptUnpublish { get; set; }
}
}

5
src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs

@ -120,6 +120,11 @@ namespace Squidex.Domain.Apps.Write.Schemas
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Unpublish(command));
}
protected Task On(ConfigureScripts command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.ConfigureScripts(command));
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))

13
src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs

@ -276,6 +276,17 @@ namespace Squidex.Domain.Apps.Write.Schemas
return this;
}
public SchemaDomainObject ConfigureScripts(ConfigureScripts command)
{
Guard.NotNull(command, nameof(command));
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured()));
return this;
}
public SchemaDomainObject Delete(DeleteSchema command)
{
VerifyCreatedAndNotDeleted();
@ -289,7 +300,7 @@ namespace Squidex.Domain.Apps.Write.Schemas
{
SimpleMapper.Map(fieldCommand, @event);
if (schema.FieldsById.TryGetValue(fieldCommand.FieldId, out Field field))
if (schema.FieldsById.TryGetValue(fieldCommand.FieldId, out var field))
{
@event.FieldId = new NamedId<long>(field.Id, field.Name);
}

36
src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs

@ -0,0 +1,36 @@
// ==========================================================================
// UserClaimsPrincipalFactoryWithEmail.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users
{
public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory<IUser, IRole>
{
public UserClaimsPrincipalFactoryWithEmail(UserManager<IUser> userManager, RoleManager<IRole> roleManager, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{
}
public override async Task<ClaimsPrincipal> CreateAsync(IUser user)
{
var principal = await base.CreateAsync(user);
principal.Identities.First().AddClaim(new Claim(OpenIdClaims.Email, await UserManager.GetEmailAsync(user)));
return principal;
}
}
}

1
src/Squidex.Domain.Users/UserExtensions.cs

@ -12,6 +12,7 @@ using Squidex.Infrastructure;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
// ReSharper disable PossibleNullReferenceException
// ReSharper disable InvertIf
namespace Squidex.Domain.Users

2
src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs

@ -276,7 +276,7 @@ namespace Squidex.Infrastructure.CQRS.Events
{
var addressParts = projectionHost.Split(':');
if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out int port))
if (addressParts.Length < 2 || !int.TryParse(addressParts[1], out var port))
{
port = 2113;
}

2
src/Squidex.Infrastructure.Redis/RedisSubscription.cs

@ -67,7 +67,7 @@ namespace Squidex.Infrastructure
return;
}
if (!Guid.TryParse(parts[0], out Guid sender))
if (!Guid.TryParse(parts[0], out var sender))
{
return;
}

4
src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs

@ -87,9 +87,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
private static IAggregateCommand GetCommand(CommandContext context)
{
var command = context.Command as IAggregateCommand;
if (command == null)
if (!(context.Command is IAggregateCommand command))
{
throw new ArgumentException("Context must have an aggregate command.", nameof(context));
}

2
src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs

@ -10,6 +10,8 @@ using System;
using System.Collections.Generic;
using Squidex.Infrastructure.CQRS.Events;
// ReSharper disable ImpureMethodCallOnReadonlyValueField
namespace Squidex.Infrastructure.CQRS
{
public abstract class DomainObjectBase : IAggregate, IEquatable<IAggregate>

47
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -60,31 +60,36 @@ namespace Squidex.Infrastructure
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary)
{
return DictionaryHashCode(dictionary, EqualityComparer<TValue>.Default);
return DictionaryHashCode(dictionary, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default);
}
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, IEqualityComparer<TValue> comparer)
public static int DictionaryHashCode<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
Guard.NotNull(comparer, nameof(comparer));
var hashCode = 17;
foreach (var kvp in dictionary.OrderBy(x => x.Key))
{
hashCode = hashCode * 23 + kvp.Key.GetHashCode();
hashCode = hashCode * 23 + keyComparer.GetHashCode(kvp.Key);
if (kvp.Value != null)
{
hashCode = hashCode * 23 + comparer.GetHashCode(kvp.Value);
hashCode = hashCode * 23 + valueComparer.GetHashCode(kvp.Value);
}
}
return hashCode;
}
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IDictionary<TKey, TValue> other)
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue> other)
{
return other != null && dictionary.Count == other.Count && !dictionary.Except(other).Any();
return EqualsDictionary(dictionary, other, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default);
}
public static bool EqualsDictionary<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, IReadOnlyDictionary<TKey, TValue> other, IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
var comparer = new KeyValuePairComparer<TKey, TValue>(keyComparer, valueComparer);
return other != null && dictionary.Count == other.Count && !dictionary.Except(other, comparer).Any();
}
public static TValue GetOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
@ -109,7 +114,7 @@ namespace Squidex.Infrastructure
public static TValue GetOrCreate<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> creator)
{
if (!dictionary.TryGetValue(key, out TValue result))
if (!dictionary.TryGetValue(key, out var result))
{
result = creator(key);
}
@ -119,7 +124,7 @@ namespace Squidex.Infrastructure
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> creator)
{
if (!dictionary.TryGetValue(key, out TValue result))
if (!dictionary.TryGetValue(key, out var result))
{
result = creator(key);
@ -136,5 +141,27 @@ namespace Squidex.Infrastructure
action(item);
}
}
public sealed class KeyValuePairComparer<TKey, TValue> : IEqualityComparer<KeyValuePair<TKey, TValue>>
{
private readonly IEqualityComparer<TKey> keyComparer;
private readonly IEqualityComparer<TValue> valueComparer;
public KeyValuePairComparer(IEqualityComparer<TKey> keyComparer, IEqualityComparer<TValue> valueComparer)
{
this.keyComparer = keyComparer;
this.valueComparer = valueComparer;
}
public bool Equals(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
{
return keyComparer.Equals(x.Key, y.Key) && valueComparer.Equals(x.Value, y.Value);
}
public int GetHashCode(KeyValuePair<TKey, TValue> obj)
{
return keyComparer.GetHashCode(obj.Key) ^ valueComparer.GetHashCode(obj.Value);
}
}
}
}

25
src/Squidex.Infrastructure/DomainForbiddenException.cs

@ -0,0 +1,25 @@
// ==========================================================================
// DomainForbiddenException.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure
{
public class DomainForbiddenException : DomainException
{
public DomainForbiddenException(string message)
: base(message)
{
}
public DomainForbiddenException(string message, Exception inner)
: base(message, inner)
{
}
}
}

2
src/Squidex.Infrastructure/DomainObjectException.cs

@ -8,6 +8,8 @@
using System;
// ReSharper disable SuggestBaseTypeForParameter
namespace Squidex.Infrastructure
{
public class DomainObjectException : Exception

2
src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs

@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Json
throw new JsonException("Named id must have more than 2 parts divided by commata");
}
if (!Guid.TryParse(parts[0], out Guid id))
if (!Guid.TryParse(parts[0], out var id))
{
throw new JsonException("Named id must be a valid guid");
}

2
src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs

@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Json
throw new JsonException("Named id must have more than 2 parts divided by commata");
}
if (!long.TryParse(parts[0], out long id))
if (!long.TryParse(parts[0], out var id))
{
throw new JsonException("Named id must be a valid long");
}

5
src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs

@ -7,7 +7,6 @@
// ==========================================================================
using System;
using System.Reflection;
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Extensions;
@ -38,9 +37,9 @@ namespace Squidex.Infrastructure.Json
var value = reader.Value;
if (value is DateTime)
if (value is DateTime dateTime)
{
properties.Set(key, ((DateTime)value).ToInstant());
properties.Set(key, dateTime.ToInstant());
}
else
{

2
src/Squidex.Infrastructure/Language.cs

@ -111,7 +111,7 @@ namespace Squidex.Infrastructure
input = match.Groups[0].Value;
}
if (TryGetLanguage(input.ToLowerInvariant(), out Language result))
if (TryGetLanguage(input.ToLowerInvariant(), out var result))
{
return result;
}

2
src/Squidex.Infrastructure/PropertiesBag.cs

@ -99,7 +99,7 @@ namespace Squidex.Infrastructure
throw new ArgumentException($"The property names '{newPropertyName}' are equal.", newPropertyName);
}
if (!internalDictionary.TryGetValue(oldPropertyName, out PropertyValue property))
if (!internalDictionary.TryGetValue(oldPropertyName, out var property))
{
return false;
}

6
src/Squidex.Infrastructure/PropertyValue.cs

@ -58,7 +58,7 @@ namespace Squidex.Infrastructure
{
result = null;
if (!Parsers.TryGetValue(binder.Type, out Func<PropertyValue, CultureInfo, object> parser))
if (!Parsers.TryGetValue(binder.Type, out var parser))
{
return false;
}
@ -145,12 +145,12 @@ namespace Squidex.Infrastructure
private T? ToNullableOrParseValue<T>(IFormatProvider culture, Func<string, T> parser) where T : struct
{
return TryParse(culture, parser, out T result) ? result : (T?)null;
return TryParse(culture, parser, out var result) ? result : (T?)null;
}
private T ToOrParseValue<T>(IFormatProvider culture, Func<string, T> parser)
{
return TryParse(culture, parser, out T result) ? result : default(T);
return TryParse(culture, parser, out var result) ? result : default(T);
}
private bool TryParse<T>(IFormatProvider culture, Func<string, T> parser, out T result)

2
src/Squidex.Infrastructure/Reflection/PropertiesTypeAccessor.cs

@ -68,7 +68,7 @@ namespace Squidex.Infrastructure.Reflection
{
Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
if (!accessors.TryGetValue(propertyName, out IPropertyAccessor accessor))
if (!accessors.TryGetValue(propertyName, out var accessor))
{
throw new ArgumentException("Property does not exist.", nameof(propertyName));
}

2
src/Squidex.Infrastructure/StringExtensions.cs

@ -356,7 +356,7 @@ namespace Squidex.Infrastructure
var lower = char.ToLowerInvariant(character);
if (LowerCaseDiacritics.TryGetValue(character, out string replacement))
if (LowerCaseDiacritics.TryGetValue(character, out var replacement))
{
if (singleCharDiactric)
{

1
src/Squidex.Infrastructure/TypeNameRegistry.cs

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
// ReSharper disable PossibleNullReferenceException
// ReSharper disable InvertIf
namespace Squidex.Infrastructure

7
src/Squidex/Config/Domain/WriteModule.cs

@ -9,13 +9,14 @@
using Autofac;
using Microsoft.Extensions.Configuration;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Write.Apps;
using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Schemas;
using Squidex.Domain.Apps.Write.Webhooks;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Pipeline.CommandHandlers;
using Squidex.Pipeline.CommandMiddlewares;
// ReSharper disable UnusedAutoPropertyAccessor.Local
@ -52,6 +53,10 @@ namespace Squidex.Config.Domain
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<JintScriptEngine>()
.As<IScriptEngine>()
.SingleInstance();
builder.RegisterType<FieldRegistry>()
.AsSelf()
.SingleInstance();

6
src/Squidex/Config/Identity/IdentityServices.cs

@ -15,6 +15,7 @@ using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Users;
@ -91,6 +92,8 @@ namespace Squidex.Config.Identity
GetApiResources());
services.AddSingleton(
GetIdentityResources());
services.AddSingleton<IUserClaimsPrincipalFactory<IUser>,
UserClaimsPrincipalFactoryWithEmail>();
services.AddSingleton<IClientStore,
LazyClientStore>();
services.AddSingleton<IResourceStore,
@ -121,6 +124,7 @@ namespace Squidex.Config.Identity
{
UserClaims = new List<string>
{
JwtClaimTypes.Email,
JwtClaimTypes.Role
}
};
@ -130,7 +134,7 @@ namespace Squidex.Config.Identity
{
yield return new IdentityResources.OpenId();
yield return new IdentityResources.Profile();
yield return new IdentityResources.Profile();
yield return new IdentityResources.Email();
yield return new IdentityResource(Constants.RoleScope,
new[]
{

1
src/Squidex/Config/Identity/IdentityUsage.cs

@ -19,6 +19,7 @@ using Squidex.Infrastructure.Log;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable InvertIf
namespace Squidex.Config.Identity

1
src/Squidex/Config/Identity/LazyClientStore.cs

@ -115,6 +115,7 @@ namespace Squidex.Config.Identity
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
Constants.ApiScope,
Constants.ProfileScope,
Constants.RoleScope

1
src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs

@ -6,7 +6,6 @@
// All rights reserved.
// ==========================================================================
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

4
src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs

@ -35,7 +35,7 @@ namespace Squidex.Config.Swagger
{
var statusCode = match.Groups["Code"].Value;
if (!operation.Responses.TryGetValue(statusCode, out SwaggerResponse response))
if (!operation.Responses.TryGetValue(statusCode, out var response))
{
response = new SwaggerResponse();
@ -72,7 +72,7 @@ namespace Squidex.Config.Swagger
private static void RemoveOkResponse(SwaggerOperation operation)
{
if (operation.Responses.TryGetValue("200", out SwaggerResponse response) &&
if (operation.Responses.TryGetValue("200", out var response) &&
response.Description != null &&
response.Description.Contains("=>"))
{

43
src/Squidex/Controllers/Api/Schemas/Models/ConfigureScriptsDto.cs

@ -0,0 +1,43 @@
// ==========================================================================
// ConfigureScriptsDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Controllers.Api.Schemas.Models
{
public sealed class ConfigureScriptsDto
{
/// <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 publishing a content.
/// </summary>
public string ScriptPublish { get; set; }
/// <summary>
/// The script that is executed when unpublishing a content.
/// </summary>
public string ScriptUnpublish { get; set; }
}
}

30
src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs

@ -33,6 +33,36 @@ namespace Squidex.Controllers.Api.Schemas.Models
/// </summary>
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 publishing a content.
/// </summary>
public string ScriptPublish { get; set; }
/// <summary>
/// The script that is executed when unpublishing a content.
/// </summary>
public string ScriptUnpublish { get; set; }
/// <summary>
/// The list of fields.
/// </summary>

24
src/Squidex/Controllers/Api/Schemas/SchemasController.cs

@ -154,6 +154,30 @@ namespace Squidex.Controllers.Api.Schemas
return NoContent();
}
/// <summary>
/// Update the scripts of 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 scripts object that needs to updated.</param>
/// <returns>
/// 204 => Schema has been updated.
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppDeveloper]
[HttpPut]
[Route("apps/{app}/schemas/{name}/scripts/")]
[ApiCosts(1)]
public async Task<IActionResult> PutSchemaScripts(string app, string name, [FromBody] ConfigureScriptsDto request)
{
var command = SimpleMapper.Map(request, new ConfigureScripts());
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Publish a schema.
/// </summary>

69
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -15,15 +15,18 @@ using Microsoft.Extensions.Primitives;
using NSwag.Annotations;
using Squidex.Controllers.ContentApi.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Contents.Commands;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
// ReSharper disable InvertIf
// ReSharper disable PossibleNullReferenceException
// ReSharper disable RedundantIfElseBlock
@ -35,17 +38,20 @@ namespace Squidex.Controllers.ContentApi
public sealed class ContentsController : ControllerBase
{
private readonly ISchemaProvider schemas;
private readonly IScriptEngine scriptEngine;
private readonly IContentRepository contentRepository;
private readonly IGraphQLService graphQL;
public ContentsController(
ICommandBus commandBus,
ISchemaProvider schemas,
IScriptEngine scriptEngine,
IContentRepository contentRepository,
IGraphQLService graphQL)
: base(commandBus)
{
this.graphQL = graphQL;
this.scriptEngine = scriptEngine;
this.schemas = schemas;
this.contentRepository = contentRepository;
}
@ -99,16 +105,27 @@ namespace Squidex.Controllers.ContentApi
await Task.WhenAll(taskForItems, taskForCount);
var scriptText = schemaEntity.ScriptQuery;
var hasScript = !string.IsNullOrWhiteSpace(scriptText);
var response = new AssetsDto
{
Total = taskForCount.Result,
Items = taskForItems.Result.Take(200).Select(x =>
Items = taskForItems.Result.Take(200).Select(item =>
{
var itemModel = SimpleMapper.Map(x, new ContentDto());
var itemModel = SimpleMapper.Map(item, new ContentDto());
if (x.Data != null)
if (item.Data != null)
{
itemModel.Data = x.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient);
var data = item.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient);
if (hasScript && !isFrontendClient)
{
data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = item.Id, User = User }, scriptText);
}
itemModel.Data = data;
}
return itemModel;
@ -144,7 +161,21 @@ namespace Squidex.Controllers.ContentApi
{
var isFrontendClient = User.IsFrontendClient();
response.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient);
var data = entity.Data.ToApiModel(schemaEntity.Schema, App.LanguagesConfig, null, !isFrontendClient);
if (!isFrontendClient)
{
var scriptText = schemaEntity.ScriptQuery;
var hasScript = !string.IsNullOrWhiteSpace(scriptText);
if (hasScript)
{
data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = entity.Id, User = User }, scriptText);
}
}
response.Data = data;
}
Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
@ -158,14 +189,14 @@ namespace Squidex.Controllers.ContentApi
[ApiCosts(1)]
public async Task<IActionResult> PostContent([FromBody] NamedContentData request, [FromQuery] bool publish = false)
{
var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish };
var command = new CreateContent { ContentId = Guid.NewGuid(), User = User, Data = request.ToCleaned(), Publish = publish };
var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<NamedContentData>>();
var response = ContentDto.Create(command, result);
return CreatedAtAction(nameof(GetContent), new { id = response.Id }, response);
return CreatedAtAction(nameof(GetContent), new { id = command.ContentId }, response);
}
[MustBeAppEditor]
@ -174,11 +205,14 @@ namespace Squidex.Controllers.ContentApi
[ApiCosts(1)]
public async Task<IActionResult> PutContent(Guid id, [FromBody] NamedContentData request)
{
var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() };
var command = new UpdateContent { ContentId = id, User = User, Data = request.ToCleaned() };
await CommandBus.PublishAsync(command);
var context = await CommandBus.PublishAsync(command);
return NoContent();
var result = context.Result<ContentDataChangedResult>();
var response = result.Data;
return Ok(response);
}
[MustBeAppEditor]
@ -187,11 +221,14 @@ namespace Squidex.Controllers.ContentApi
[ApiCosts(1)]
public async Task<IActionResult> PatchContent(Guid id, [FromBody] NamedContentData request)
{
var command = new PatchContent { ContentId = id, Data = request.ToCleaned() };
var command = new PatchContent { ContentId = id, User = User, Data = request.ToCleaned() };
await CommandBus.PublishAsync(command);
var context = await CommandBus.PublishAsync(command);
return NoContent();
var result = context.Result<ContentDataChangedResult>();
var response = result.Data;
return Ok(response);
}
[MustBeAppEditor]
@ -200,7 +237,7 @@ namespace Squidex.Controllers.ContentApi
[ApiCosts(1)]
public async Task<IActionResult> PublishContent(Guid id)
{
var command = new PublishContent { ContentId = id };
var command = new PublishContent { ContentId = id, User = User };
await CommandBus.PublishAsync(command);
@ -213,7 +250,7 @@ namespace Squidex.Controllers.ContentApi
[ApiCosts(1)]
public async Task<IActionResult> UnpublishContent(Guid id)
{
var command = new UnpublishContent { ContentId = id };
var command = new UnpublishContent { ContentId = id, User = User };
await CommandBus.PublishAsync(command);
@ -226,7 +263,7 @@ namespace Squidex.Controllers.ContentApi
[ApiCosts(1)]
public async Task<IActionResult> PutContent(Guid id)
{
var command = new DeleteContent { ContentId = id };
var command = new DeleteContent { ContentId = id, User = User };
await CommandBus.PublishAsync(command);

4
src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs

@ -165,7 +165,7 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddResponse("204", $"{schemaName} element updated.");
operation.AddResponse("201", $"{schemaName} element updated.", dataSchema);
operation.Security = EditorSecurity;
});
@ -180,7 +180,7 @@ namespace Squidex.Controllers.ContentApi.Generator
operation.AddBodyParameter("data", contentSchema, SchemaBodyDescription);
operation.AddResponse("204", $"{schemaName} element updated.");
operation.AddResponse("201", $"{schemaName} element patched.", dataSchema);
operation.Security = EditorSecurity;
});

2
src/Squidex/Controllers/ContentApi/Models/ContentDto.cs

@ -54,7 +54,7 @@ namespace Squidex.Controllers.ContentApi.Models
/// <summary>
/// Indicates if the content element is publihed.
/// </summary>
public bool IsPublished { get; set; }
public bool? IsPublished { get; set; }
/// <summary>
/// The version of the content.

13
src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs

@ -24,18 +24,14 @@ namespace Squidex.Pipeline
private static void AddHandler<T>(Func<T, IActionResult> handler) where T : Exception
{
Handlers.Add(ex =>
{
var typed = ex as T;
return typed != null ? handler(typed) : null;
});
Handlers.Add(ex => ex is T typed ? handler(typed) : null);
}
static ApiExceptionFilterAttribute()
{
AddHandler<DomainObjectNotFoundException>(OnDomainObjectNotFoundException);
AddHandler<DomainObjectVersionException>(OnDomainObjectVersionException);
AddHandler<DomainForbiddenException>(OnDomainForbiddenException);
AddHandler<DomainException>(OnDomainException);
AddHandler<ValidationException>(OnValidationException);
}
@ -55,6 +51,11 @@ namespace Squidex.Pipeline
return ErrorResult(400, new ErrorDto { Message = ex.Message });
}
private static IActionResult OnDomainForbiddenException(DomainForbiddenException ex)
{
return ErrorResult(403, new ErrorDto { Message = ex.Message });
}
private static IActionResult OnValidationException(ValidationException ex)
{
return ErrorResult(400, new ErrorDto { Message = ex.Message, Details = ex.Errors.Select(e => e.Message).ToArray() });

4
src/Squidex/Pipeline/CommandHandlers/ETagCommandMiddleware.cs → src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs

@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Pipeline.CommandHandlers
namespace Squidex.Pipeline.CommandMiddlewares
{
public class ETagCommandMiddleware : ICommandMiddleware
{
@ -29,7 +29,7 @@ namespace Squidex.Pipeline.CommandHandlers
var headers = httpContextAccessor.HttpContext.Request.Headers;
var headerMatch = headers["If-Match"].ToString();
if (!string.IsNullOrWhiteSpace(headerMatch) && long.TryParse(headerMatch, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion))
if (!string.IsNullOrWhiteSpace(headerMatch) && long.TryParse(headerMatch, NumberStyles.Any, CultureInfo.InvariantCulture, out var expectedVersion))
{
context.Command.ExpectedVersion = expectedVersion;
}

2
src/Squidex/Pipeline/CommandHandlers/EnrichWithActorCommandMiddleware.cs → src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs

@ -17,7 +17,7 @@ using Squidex.Infrastructure.Security;
// ReSharper disable InvertIf
namespace Squidex.Pipeline.CommandHandlers
namespace Squidex.Pipeline.CommandMiddlewares
{
public class EnrichWithActorCommandMiddleware : ICommandMiddleware
{

2
src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdCommandMiddleware.cs → src/Squidex/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs

@ -15,7 +15,7 @@ using Squidex.Infrastructure.CQRS.Commands;
// ReSharper disable InvertIf
namespace Squidex.Pipeline.CommandHandlers
namespace Squidex.Pipeline.CommandMiddlewares
{
public sealed class EnrichWithAppIdCommandMiddleware : ICommandMiddleware
{

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

@ -18,7 +18,7 @@ using Squidex.Infrastructure.CQRS.Commands;
// ReSharper disable InvertIf
namespace Squidex.Pipeline.CommandHandlers
namespace Squidex.Pipeline.CommandMiddlewares
{
public sealed class EnrichWithSchemaIdCommandMiddleware : ICommandMiddleware
{

2
src/Squidex/Pipeline/Swagger/SwaggerHelper.cs

@ -97,7 +97,7 @@ namespace Squidex.Pipeline.Swagger
{ SquidexRoles.AppOwner, "App contributor with Owner permission." },
{ SquidexRoles.AppEditor, "Client (writer) or App contributor with Editor permission." },
{ SquidexRoles.AppReader, "Client (readonly) or App contributor with Editor permission." },
{ SquidexRoles.AppDeveloper, "App contributor with Developer permission." },
{ SquidexRoles.AppDeveloper, "App contributor with Developer permission." }
},
Description = securityDescription
};

2
src/Squidex/Squidex.csproj

@ -71,7 +71,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" />
<PackageReference Include="MongoDB.Driver" Version="2.4.4" />
<PackageReference Include="NJsonSchema" Version="9.5.0" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />

5
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -129,12 +129,13 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
} else {
this.appNameOnce()
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, requestDto, this.version))
.subscribe(() => {
this.content = this.content.update(requestDto, this.authService.user.token);
.subscribe(dto => {
this.content = this.content.update(dto, this.authService.user.token);
this.emitContentUpdated(this.content);
this.notifyInfo('Content saved successfully.');
this.enableContentForm();
this.populateContentForm();
}, error => {
this.notifyError(error);
this.enableContentForm();

1
src/Squidex/app/features/schemas/declarations.ts

@ -24,5 +24,6 @@ export * from './pages/schema/types/string-validation.component';
export * from './pages/schema/field.component';
export * from './pages/schema/schema-edit-form.component';
export * from './pages/schema/schema-page.component';
export * from './pages/schema/schema-scripts-form.component';
export * from './pages/schemas/schema-form.component';
export * from './pages/schemas/schemas-page.component';

2
src/Squidex/app/features/schemas/module.ts

@ -37,6 +37,7 @@ import {
SchemaFormComponent,
SchemaPageComponent,
SchemasPageComponent,
SchemaScriptsFormComponent,
StringUIComponent,
StringValidationComponent
} from './declarations';
@ -101,6 +102,7 @@ const routes: Routes = [
SchemaEditFormComponent,
SchemaFormComponent,
SchemaPageComponent,
SchemaScriptsFormComponent,
SchemasPageComponent,
StringUIComponent,
StringValidationComponent

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

@ -102,8 +102,8 @@ export class SchemaEditFormComponent extends ComponentBase implements OnInit {
}
private resetEditForm() {
this.editFormSubmitted = false;
this.editForm.reset();
this.editForm.enable();
this.editFormSubmitted = false;
}
}

12
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -22,6 +22,9 @@
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown" closeAlways="true" [sqxModalTarget]="optionsButton" position="right" [@fade]>
<a class="dropdown-item" (click)="configureScriptsDialog.show()">
Scripts
</a>
<a class="dropdown-item dropdown-item-delete" (sqxConfirmClick)="deleteSchema()" confirmTitle="Delete schema" confirmText="Do you really want to delete the schema?">
Delete
</a>
@ -141,4 +144,13 @@
</div>
</div>
<div class="modal" *sqxModalView="configureScriptsDialog;onRoot:true" [@fade]>
<div class="modal-backdrop"></div>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<sqx-schema-scripts-form [appName]="appName() | async" [schema]="schema" (saved)="onSchemaScriptsSaved($event)" (cancelled)="configureScriptsDialog.hide()"></sqx-schema-scripts-form>
</div>
</div>
</div>
<router-outlet></router-outlet>

9
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -27,6 +27,7 @@ import {
SchemaPropertiesDto,
SchemasService,
UpdateFieldDto,
UpdateSchemaScriptsDto,
ValidatorsEx
} from 'shared';
@ -49,6 +50,8 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
public exportSchemaDialog = new ModalView();
public configureScriptsDialog = new ModalView();
public editOptionsDropdown = new ModalView();
public editSchemaDialog = new ModalView();
@ -239,6 +242,12 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
this.editSchemaDialog.hide();
}
public onSchemaScriptsSaved(scripts: UpdateSchemaScriptsDto) {
this.updateSchema(this.schema.configureScripts(scripts, this.authService.user.token));
this.configureScriptsDialog.hide();
}
private resetFieldForm() {
this.addFieldForm.enable();
this.addFieldForm.reset({ type: 'String' });

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

@ -0,0 +1,33 @@
<form [formGroup]="editForm" (ngSubmit)="saveSchema()">
<div class="modal-header">
<h4 class="modal-title">Scripts</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs">
<li class="nav-item" *ngFor="let script of scripts">
<a class="nav-link" [class.active]="selectedField === 'script' + script" (click)="selectField('script' + script)">{{script}}</a>
</li>
</ul>
<div class="form-group">
<div *ngFor="let script of scripts">
<div *ngIf="selectedField === 'script' + script">
<sqx-jscript-editor name="script" [formControlName]="'script' + script"></sqx-jscript-editor>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()" [disabled]="editFormSubmitted">Cancel</button>
<button type="submit" class="float-right btn btn-primary">Save</button>
</div>
</div>
</form>

14
src/Squidex/app/features/schemas/pages/schema/schema-scripts-form.component.scss

@ -0,0 +1,14 @@
@import '_vars';
@import '_mixins';
.nav-link {
cursor: default;
}
.nav-tabs {
border: 0;
}
.clearfix {
width: 100%;
}

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

@ -0,0 +1,123 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import {
ComponentBase,
DialogService,
SchemaDetailsDto,
SchemasService,
UpdateSchemaScriptsDto
} from 'shared';
@Component({
selector: 'sqx-schema-scripts-form',
styleUrls: ['./schema-scripts-form.component.scss'],
templateUrl: './schema-scripts-form.component.html'
})
export class SchemaScriptsFormComponent extends ComponentBase implements OnInit {
@Output()
public saved = new EventEmitter<UpdateSchemaScriptsDto>();
@Output()
public cancelled = new EventEmitter();
@Input()
public schema: SchemaDetailsDto;
@Input()
public appName: string;
public selectedField = 'scriptQuery';
public scripts = [
'Query',
'Create',
'Update',
'Delete',
'Publish',
'Unpublish'
];
public editFormSubmitted = false;
public editForm =
this.formBuilder.group({
scriptQuery: '',
scriptCreate: '',
scriptUpdate: '',
scriptDelete: '',
scriptPublish: '',
scriptUnpublish: ''
});
constructor(dialogs: DialogService,
private readonly schemas: SchemasService,
private readonly formBuilder: FormBuilder
) {
super(dialogs);
}
public ngOnInit() {
this.editForm.patchValue(this.schema);
}
public cancel() {
this.emitCancelled();
this.resetEditForm();
}
public saveSchema() {
this.editFormSubmitted = true;
if (this.editForm.valid) {
this.editForm.disable();
const requestDto =
new UpdateSchemaScriptsDto(
this.editForm.controls['scriptQuery'].value,
this.editForm.controls['scriptCreate'].value,
this.editForm.controls['scriptUpdate'].value,
this.editForm.controls['scriptDelete'].value,
this.editForm.controls['scriptPublish'].value,
this.editForm.controls['scriptUnpublish'].value);
this.schemas.putSchemaScripts(this.appName, this.schema.name, requestDto, this.schema.version)
.subscribe(dto => {
this.emitSaved(requestDto);
this.resetEditForm();
}, error => {
this.notifyError(error);
this.enableEditForm();
});
}
}
private emitCancelled() {
this.cancelled.emit();
}
private emitSaved(requestDto: UpdateSchemaScriptsDto) {
this.saved.emit(requestDto);
}
public selectField(field: string) {
this.selectedField = field;
}
private enableEditForm() {
this.editForm.enable();
this.editFormSubmitted = false;
}
private resetEditForm() {
this.editForm.reset();
this.editForm.enable();
this.editFormSubmitted = false;
}
}

6
src/Squidex/app/framework/angular/dialog-renderer.component.html

@ -11,8 +11,10 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="confirm()">Yes</button>
<button type="button" class="btn btn-secondary" (click)="cancel()">No</button>
<div class="clearfix">
<button type="button" class="float-left btn btn-secondary" (click)="cancel()">No</button>
<button type="button" class="float-right btn btn-danger" (click)="confirm()">Yes</button>
</div>
</div>
</div>
</div>

4
src/Squidex/app/framework/angular/dialog-renderer.component.scss

@ -25,4 +25,8 @@
&-bottomleft {
@include fixed(auto, auto, 0, 0);
}
}
.clearfix {
width: 100%;
}

1
src/Squidex/app/framework/angular/jscript-editor.component.html

@ -0,0 +1 @@
<div class="editor" #editor></div>

8
src/Squidex/app/framework/angular/jscript-editor.component.scss

@ -0,0 +1,8 @@
@import '_mixins';
@import '_vars';
.editor {
background: $color-dark-foreground;
border: 1px solid $color-input;
height: 20rem;
}

108
src/Squidex/app/framework/angular/jscript-editor.component.ts

@ -0,0 +1,108 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { AfterViewInit, Component, forwardRef, ElementRef, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs';
import { ResourceLoaderService } from './../services/resource-loader.service';
declare var ace: any;
const NOOP = () => { /* NOOP */ };
export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JscriptEditorComponent), multi: true
};
@Component({
selector: 'sqx-jscript-editor',
styleUrls: ['./jscript-editor.component.scss'],
templateUrl: './jscript-editor.component.html',
providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class JscriptEditorComponent implements ControlValueAccessor, AfterViewInit {
private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP;
private valueChanged = new Subject();
private aceEditor: any;
private oldValue: string;
private isDisabled = false;
@ViewChild('editor')
public editor: ElementRef;
constructor(
private readonly resourceLoader: ResourceLoaderService
) {
}
public writeValue(value: any) {
this.oldValue = value;
if (this.aceEditor) {
this.setValue(value);
}
}
public setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
if (this.aceEditor) {
this.aceEditor.setReadOnly(isDisabled);
}
}
public registerOnChange(fn: any) {
this.changeCallback = fn;
}
public registerOnTouched(fn: any) {
this.touchedCallback = fn;
}
public ngAfterViewInit() {
this.valueChanged.debounceTime(500)
.subscribe(() => {
this.changeValue();
});
this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js').then(() => {
this.aceEditor = ace.edit(this.editor.nativeElement);
this.aceEditor.getSession().setMode('ace/mode/javascript');
this.aceEditor.setReadOnly(this.isDisabled);
this.aceEditor.setFontSize(14);
this.setValue(this.oldValue);
this.aceEditor.on('blur', () => {
this.changeValue();
this.touchedCallback();
});
this.aceEditor.on('change', () => {
this.valueChanged.next();
});
});
}
private changeValue() {
const newValue = this.aceEditor.getValue();
if (this.oldValue !== newValue) {
this.changeCallback(newValue);
}
this.oldValue = newValue;
}
private setValue(value: string) {
this.aceEditor.setValue(value || '');
this.aceEditor.clearSelection();
}
}

2
src/Squidex/app/framework/angular/json-editor.component.scss

@ -4,5 +4,5 @@
.editor {
background: $color-dark-foreground;
border: 1px solid $color-input;
height: 30rem;
height: 20rem;
}

1
src/Squidex/app/framework/declarations.ts

@ -21,6 +21,7 @@ export * from './angular/geolocation-editor.component';
export * from './angular/http-extensions-impl';
export * from './angular/image-source.directive';
export * from './angular/indeterminate-value.directive';
export * from './angular/jscript-editor.component';
export * from './angular/json-editor.component';
export * from './angular/lowercase-input.directive';
export * from './angular/markdown-editor.component';

3
src/Squidex/app/framework/module.ts

@ -33,6 +33,7 @@ import {
GeolocationEditorComponent,
ImageSourceDirective,
IndeterminateValueDirective,
JscriptEditorComponent,
JsonEditorComponent,
KNumberPipe,
LocalCacheService,
@ -96,6 +97,7 @@ import {
GeolocationEditorComponent,
ImageSourceDirective,
IndeterminateValueDirective,
JscriptEditorComponent,
JsonEditorComponent,
KNumberPipe,
LowerCaseInputDirective,
@ -143,6 +145,7 @@ import {
GeolocationEditorComponent,
ImageSourceDirective,
IndeterminateValueDirective,
JscriptEditorComponent,
JsonEditorComponent,
KNumberPipe,
LowerCaseInputDirective,

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

Loading…
Cancel
Save