Browse Source

Consistent intellisense (#860)

* First attempt.

* Typed script vars and annotations.

* Use intellisense for rule actions.

* Fix tests.

* Stupid mini fix.
pull/861/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
f5073a56bc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs
  2. 5
      backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs
  3. 2
      backend/i18n/frontend_en.json
  4. 2
      backend/i18n/frontend_it.json
  5. 2
      backend/i18n/frontend_nl.json
  6. 2
      backend/i18n/frontend_zh.json
  7. 2
      backend/i18n/source/frontend_en.json
  8. 20
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptionAttribute.cs
  9. 156
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs
  10. 60
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx
  11. 29
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs
  12. 5
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs
  13. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs
  14. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs
  15. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs
  16. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs
  18. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs
  19. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  20. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx
  22. 82
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs
  23. 83
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs
  24. 57
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs
  25. 101
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs
  26. 20
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs
  27. 19
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs
  28. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  29. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
  30. 125
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs
  31. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  32. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  33. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs
  34. 18
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  35. 331
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs
  36. 9
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  37. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  38. 198
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs
  39. 5
      backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
  40. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  41. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  42. 115
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs
  43. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  44. 39
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  45. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  46. 3
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
  47. 10
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs
  48. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  49. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs
  50. 8
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  51. 1
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  52. 15
      backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  53. 10
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  54. 2
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  55. 1
      backend/src/Squidex/Config/Domain/QueryServices.cs
  56. 38
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  57. 380
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs
  58. 82
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs
  59. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  60. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs
  61. 7
      frontend/src/app/features/rules/pages/rule/rule-page.component.html
  62. 2
      frontend/src/app/features/rules/shared/actions/formattable-input.component.html
  63. 2
      frontend/src/app/features/rules/shared/actions/formattable-input.component.scss
  64. 7
      frontend/src/app/features/rules/shared/actions/formattable-input.component.ts
  65. 8
      frontend/src/app/features/rules/shared/actions/generic-action.component.html
  66. 35
      frontend/src/app/features/rules/shared/actions/generic-action.component.ts
  67. 2
      frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts
  68. 2
      frontend/src/app/features/settings/pages/backups/backups-page.component.ts
  69. 5
      frontend/src/app/framework/angular/markdown.directive.ts
  70. 20
      frontend/src/app/shared/services/assets.service.spec.ts
  71. 19
      frontend/src/app/shared/services/rules.service.spec.ts
  72. 9
      frontend/src/app/shared/services/rules.service.ts
  73. 19
      frontend/src/app/shared/services/schemas.service.spec.ts
  74. 8
      frontend/src/app/shared/state/apps.state.ts
  75. 12
      frontend/src/app/shared/state/asset-scripts.state.ts
  76. 12
      frontend/src/app/shared/state/asset-uploader.state.ts
  77. 20
      frontend/src/app/shared/state/assets.state.ts
  78. 12
      frontend/src/app/shared/state/backups.state.ts
  79. 12
      frontend/src/app/shared/state/clients.state.ts
  80. 8
      frontend/src/app/shared/state/contents.state.ts
  81. 8
      frontend/src/app/shared/state/contributors.state.ts
  82. 12
      frontend/src/app/shared/state/languages.state.ts
  83. 12
      frontend/src/app/shared/state/plans.state.ts
  84. 12
      frontend/src/app/shared/state/roles.state.ts
  85. 12
      frontend/src/app/shared/state/rule-events.state.ts
  86. 12
      frontend/src/app/shared/state/rule-simulator.state.ts
  87. 12
      frontend/src/app/shared/state/rules.state.ts
  88. 12
      frontend/src/app/shared/state/schemas.state.ts
  89. 12
      frontend/src/app/shared/state/ui.state.ts
  90. 12
      frontend/src/app/shared/state/workflows.state.ts

5
backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs

@ -18,9 +18,10 @@ namespace Squidex.Extensions.Actions
{
if (!string.IsNullOrWhiteSpace(expression))
{
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
["event"] = @event
Event = @event
};
return scriptEngine.Evaluate(vars, expression);

5
backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs

@ -33,9 +33,10 @@ namespace Squidex.Extensions.Actions.Script
protected override async Task<Result> ExecuteJobAsync(ScriptJob job,
CancellationToken ct = default)
{
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
["event"] = job.Event
Event = job.Event
};
var result = await scriptEngine.ExecuteAsync(vars, job.Script, ct: ct);

2
backend/i18n/frontend_en.json

@ -261,6 +261,7 @@
"common.disable": "Disable",
"common.disabled": "Disabled",
"common.displayName": "Display Name",
"common.documentation": "Documentation",
"common.edit": "Edit",
"common.editing": "Editing",
"common.email": "Email",
@ -658,6 +659,7 @@
"rules.actionData": "Action Data",
"rules.actionHint": "The selection of the action type cannot be changed later.",
"rules.addSchema": "Add Schema",
"rules.advancedFormattingHint": "You can use advanced formatting",
"rules.cancelFailed": "Failed to cancel rule. Please reload.",
"rules.conditionHint": "Optional condition as javascript expression",
"rules.conditionHint2": "Conditions are javascript expressions that define when to trigger, for example",

2
backend/i18n/frontend_it.json

@ -261,6 +261,7 @@
"common.disable": "Disable",
"common.disabled": "Disabled",
"common.displayName": "Nome visualizzato",
"common.documentation": "Documentation",
"common.edit": "Modifica",
"common.editing": "Editing",
"common.email": "Email",
@ -658,6 +659,7 @@
"rules.actionData": "Action Data",
"rules.actionHint": "The selection of the action type cannot be changed later.",
"rules.addSchema": "Add Schema",
"rules.advancedFormattingHint": "You can use advanced formatting",
"rules.cancelFailed": "Non è stato possibile eliminare la regola. Per favore ricarica.",
"rules.conditionHint": "Optional condition as javascript expression",
"rules.conditionHint2": "Conditions are javascript expressions that define when to trigger, for example",

2
backend/i18n/frontend_nl.json

@ -261,6 +261,7 @@
"common.disable": "Uitzetten",
"common.disabled": "Uitgezet",
"common.displayName": "Weergavenaam",
"common.documentation": "Documentation",
"common.edit": "Bewerken",
"common.editing": "Bewerken",
"common.email": "E-mail",
@ -658,6 +659,7 @@
"rules.actionData": "Actiegegevens",
"rules.actionHint": "De selectie van het actietype kan later niet worden gewijzigd.",
"rules.addSchema": "Add Schema",
"rules.advancedFormattingHint": "You can use advanced formatting",
"rules.cancelFailed": "Annuleren van regel is mislukt. Laad opnieuw.",
"rules.conditionHint": "Optional condition as javascript expression",
"rules.conditionHint2": "Conditions are javascript expressions that define when to trigger, for example",

2
backend/i18n/frontend_zh.json

@ -261,6 +261,7 @@
"common.disable": "Disable",
"common.disabled": "已禁用",
"common.displayName": "显示名称",
"common.documentation": "Documentation",
"common.edit": "编辑",
"common.editing": "Editing",
"common.email": "电子邮件",
@ -658,6 +659,7 @@
"rules.actionData": "动作数据",
"rules.actionHint": "动作类型的选择以后不能更改。",
"rules.addSchema": "Add Schema",
"rules.advancedFormattingHint": "You can use advanced formatting",
"rules.cancelFailed": "取消规则失败,请重新加载。",
"rules.conditionHint": "Optional condition as javascript expression",
"rules.conditionHint2": "Conditions are javascript expressions that define when to trigger, for example",

2
backend/i18n/source/frontend_en.json

@ -261,6 +261,7 @@
"common.disable": "Disable",
"common.disabled": "Disabled",
"common.displayName": "Display Name",
"common.documentation": "Documentation",
"common.edit": "Edit",
"common.editing": "Editing",
"common.email": "Email",
@ -658,6 +659,7 @@
"rules.actionData": "Action Data",
"rules.actionHint": "The selection of the action type cannot be changed later.",
"rules.addSchema": "Add Schema",
"rules.advancedFormattingHint": "You can use advanced formatting",
"rules.cancelFailed": "Failed to cancel rule. Please reload.",
"rules.conditionHint": "Optional condition as javascript expression",
"rules.conditionHint2": "Conditions are javascript expressions that define when to trigger, for example",

20
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptionAttribute.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core
{
[AttributeUsage(AttributeTargets.Property)]
public sealed class FieldDescriptionAttribute : Attribute
{
public string Name { get; }
public FieldDescriptionAttribute(string name)
{
Name = name;
}
}
}

156
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs

@ -60,6 +60,33 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The user or client that triggered the event or command..
/// </summary>
public static string Actor {
get {
return ResourceManager.GetString("Actor", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The ID of this actor..
/// </summary>
public static string ActorIdentifier {
get {
return ResourceManager.GetString("ActorIdentifier", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The type of this actor..
/// </summary>
public static string ActorType {
get {
return ResourceManager.GetString("ActorType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The ID of the current app..
/// </summary>
@ -303,6 +330,33 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The user when someone is mentioned with &apos;@&apos;..
/// </summary>
public static string CommentMentionedUser {
get {
return ResourceManager.GetString("CommentMentionedUser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The text of the comment..
/// </summary>
public static string CommentText {
get {
return ResourceManager.GetString("CommentText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The URL pointing to the source of the comment..
/// </summary>
public static string CommentUrl {
get {
return ResourceManager.GetString("CommentUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} field ({1} component)..
/// </summary>
@ -321,6 +375,15 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The content..
/// </summary>
public static string Content {
get {
return ResourceManager.GetString("Content", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} nested field..
/// </summary>
@ -627,6 +690,42 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The actual event..
/// </summary>
public static string Event {
get {
return ResourceManager.GetString("Event", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The name of the event..
/// </summary>
public static string EventName {
get {
return ResourceManager.GetString("EventName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The event timestamp..
/// </summary>
public static string EventTimestamp {
get {
return ResourceManager.GetString("EventTimestamp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The type of the event, e.g. &apos;Created&apos; or &apos;Updated&apos;..
/// </summary>
public static string EventType {
get {
return ResourceManager.GetString("EventType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The path to the json value..
/// </summary>
@ -636,6 +735,24 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The ID part of this ID..
/// </summary>
public static string NamedId {
get {
return ResourceManager.GetString("NamedId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The name part of this ID..
/// </summary>
public static string NamedName {
get {
return ResourceManager.GetString("NamedName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The current operation..
/// </summary>
@ -717,6 +834,33 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The ID of the schema..
/// </summary>
public static string SchemaId {
get {
return ResourceManager.GetString("SchemaId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The current number of calls..
/// </summary>
public static string UsageCallsCurrent {
get {
return ResourceManager.GetString("UsageCallsCurrent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The configured usage limit..
/// </summary>
public static string UsageCallsLimit {
get {
return ResourceManager.GetString("UsageCallsLimit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Information about the current user..
/// </summary>
@ -727,7 +871,7 @@ namespace Squidex.Domain.Apps.Core {
}
/// <summary>
/// Looks up a localized string similar to The additional properties of the user..
/// Looks up a localized string similar to The additional properties of this user..
/// </summary>
public static string UserClaims {
get {
@ -736,7 +880,7 @@ namespace Squidex.Domain.Apps.Core {
}
/// <summary>
/// Looks up a localized string similar to The display name of the user..
/// Looks up a localized string similar to The display name of this user..
/// </summary>
public static string UserDisplayName {
get {
@ -745,7 +889,7 @@ namespace Squidex.Domain.Apps.Core {
}
/// <summary>
/// Looks up a localized string similar to The email address of the current user..
/// Looks up a localized string similar to The email address ofthis user..
/// </summary>
public static string UserEmail {
get {
@ -754,7 +898,7 @@ namespace Squidex.Domain.Apps.Core {
}
/// <summary>
/// Looks up a localized string similar to The ID of the user..
/// Looks up a localized string similar to The ID of this user..
/// </summary>
public static string UserId {
get {
@ -763,7 +907,7 @@ namespace Squidex.Domain.Apps.Core {
}
/// <summary>
/// Looks up a localized string similar to True when the current user is a client, which is typically the case when the request is made from the API..
/// Looks up a localized string similar to True when this user is a client, which is typically the case when the request is made from the API..
/// </summary>
public static string UserIsClient {
get {
@ -772,7 +916,7 @@ namespace Squidex.Domain.Apps.Core {
}
/// <summary>
/// Looks up a localized string similar to True when the current user is a user, which is typically the case when the request is made in the UI..
/// Looks up a localized string similar to True when this user is a user, which is typically the case when the request is made in the UI..
/// </summary>
public static string UserIsUser {
get {

60
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx

@ -117,6 +117,15 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Actor" xml:space="preserve">
<value>The user or client that triggered the event or command.</value>
</data>
<data name="ActorIdentifier" xml:space="preserve">
<value>The ID of this actor.</value>
</data>
<data name="ActorType" xml:space="preserve">
<value>The type of this actor.</value>
</data>
<data name="AppId" xml:space="preserve">
<value>The ID of the current app.</value>
</data>
@ -198,12 +207,24 @@
<data name="Command" xml:space="preserve">
<value>The executed command.</value>
</data>
<data name="CommentMentionedUser" xml:space="preserve">
<value>The user when someone is mentioned with '@'.</value>
</data>
<data name="CommentText" xml:space="preserve">
<value>The text of the comment.</value>
</data>
<data name="CommentUrl" xml:space="preserve">
<value>The URL pointing to the source of the comment.</value>
</data>
<data name="ComponentField" xml:space="preserve">
<value>{0} field ({1} component).</value>
</data>
<data name="ComponentSchemaId" xml:space="preserve">
<value>The schema id to identity the component type.</value>
</data>
<data name="Content" xml:space="preserve">
<value>The content.</value>
</data>
<data name="ContentArrayField" xml:space="preserve">
<value>{0} nested field.</value>
</data>
@ -306,9 +327,27 @@
<data name="EntityVersion" xml:space="preserve">
<value>The version of the objec.</value>
</data>
<data name="Event" xml:space="preserve">
<value>The actual event.</value>
</data>
<data name="EventName" xml:space="preserve">
<value>The name of the event.</value>
</data>
<data name="EventTimestamp" xml:space="preserve">
<value>The event timestamp.</value>
</data>
<data name="EventType" xml:space="preserve">
<value>The type of the event, e.g. 'Created' or 'Updated'.</value>
</data>
<data name="JsonPath" xml:space="preserve">
<value>The path to the json value.</value>
</data>
<data name="NamedId" xml:space="preserve">
<value>The ID part of this ID.</value>
</data>
<data name="NamedName" xml:space="preserve">
<value>The name part of this ID.</value>
</data>
<data name="Operation" xml:space="preserve">
<value>The current operation.</value>
</data>
@ -336,26 +375,35 @@
<data name="QueryVersion" xml:space="preserve">
<value>The optional version of the content to retrieve an older instance (not cached).</value>
</data>
<data name="SchemaId" xml:space="preserve">
<value>The ID of the schema.</value>
</data>
<data name="UsageCallsCurrent" xml:space="preserve">
<value>The current number of calls.</value>
</data>
<data name="UsageCallsLimit" xml:space="preserve">
<value>The configured usage limit.</value>
</data>
<data name="User" xml:space="preserve">
<value>Information about the current user.</value>
</data>
<data name="UserClaims" xml:space="preserve">
<value>The additional properties of the user.</value>
<value>The additional properties of this user.</value>
</data>
<data name="UserDisplayName" xml:space="preserve">
<value>The display name of the user.</value>
<value>The display name of this user.</value>
</data>
<data name="UserEmail" xml:space="preserve">
<value>The email address of the current user.</value>
<value>The email address ofthis user.</value>
</data>
<data name="UserId" xml:space="preserve">
<value>The ID of the user.</value>
<value>The ID of this user.</value>
</data>
<data name="UserIsClient" xml:space="preserve">
<value>True when the current user is a client, which is typically the case when the request is made from the API.</value>
<value>True when this user is a client, which is typically the case when the request is made from the API.</value>
</data>
<data name="UserIsUser" xml:space="preserve">
<value>True when the current user is a user, which is typically the case when the request is made in the UI.</value>
<value>True when this user is a user, which is typically the case when the request is made in the UI.</value>
</data>
<data name="UsersClaimsValue" xml:space="preserve">
<value>The list of additional properties that have the name 'name'.</value>

29
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs

@ -13,32 +13,61 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedAssetEvent : EnrichedUserEventBase, IEnrichedEntityEvent
{
[FieldDescription(nameof(FieldDescriptions.EventType))]
public EnrichedAssetEventType Type { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityId))]
public DomainId Id { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityCreated))]
public Instant Created { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityLastModified))]
public Instant LastModified { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityCreatedBy))]
public RefToken CreatedBy { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityLastModifiedBy))]
public RefToken LastModifiedBy { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetParentId))]
public DomainId ParentId { get; }
[FieldDescription(nameof(FieldDescriptions.AssetMimeType))]
public string MimeType { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetFileName))]
public string FileName { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetFileHash))]
public string FileHash { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetSlug))]
public string Slug { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetFileVersion))]
public long FileVersion { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetFileSize))]
public long FileSize { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetIsProtected))]
public bool IsProtected { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetPixelWidth))]
public int? PixelWidth { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetPixelHeight))]
public int? PixelHeight { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetType))]
public AssetType AssetType { get; set; }
[FieldDescription(nameof(FieldDescriptions.AssetMetadata))]
public AssetMetadata Metadata { get; }
[FieldDescription(nameof(FieldDescriptions.AssetIsImage))]
public bool IsImage
{
get => AssetType == AssetType.Image;

5
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs

@ -9,16 +9,19 @@ using System.Runtime.Serialization;
using Squidex.Shared.Users;
#pragma warning disable CA1822 // Mark members as static
#pragma warning disable SA1133 // Do not combine attributes
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedCommentEvent : EnrichedUserEventBase
{
[FieldDescription(nameof(FieldDescriptions.CommentText))]
public string Text { get; set; }
[FieldDescription(nameof(FieldDescriptions.CommentUrl))]
public Uri? Url { get; set; }
[IgnoreDataMember]
[FieldDescription(nameof(FieldDescriptions.CommentMentionedUser)), IgnoreDataMember]
public IUser MentionedUser { get; set; }
[IgnoreDataMember]

10
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedContentEvent.cs

@ -13,24 +13,34 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedContentEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent
{
[FieldDescription(nameof(FieldDescriptions.EventType))]
public EnrichedContentEventType Type { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityId))]
public DomainId Id { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityCreated))]
public Instant Created { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityLastModified))]
public Instant LastModified { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityCreatedBy))]
public RefToken CreatedBy { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityLastModifiedBy))]
public RefToken LastModifiedBy { get; set; }
[FieldDescription(nameof(FieldDescriptions.ContentData))]
public ContentData Data { get; set; }
[FieldDescription(nameof(FieldDescriptions.ContentDataOld))]
public ContentData? DataOld { get; set; }
[FieldDescription(nameof(FieldDescriptions.ContentStatus))]
public Status Status { get; set; }
[FieldDescription(nameof(FieldDescriptions.ContentNewStatus))]
public Status? NewStatus { get; set; }
public override long Partition

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedEvent.cs

@ -12,12 +12,16 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public abstract class EnrichedEvent
{
[FieldDescription(nameof(FieldDescriptions.AppId))]
public NamedId<DomainId> AppId { get; set; }
[FieldDescription(nameof(FieldDescriptions.EventTimestamp))]
public Instant Timestamp { get; set; }
[FieldDescription(nameof(FieldDescriptions.EventName))]
public string Name { get; set; }
[FieldDescription(nameof(FieldDescriptions.EntityVersion))]
public long Version { get; set; }
public abstract long Partition { get; }

1
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEvent.cs

@ -11,6 +11,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedSchemaEvent : EnrichedSchemaEventBase, IEnrichedEntityEvent
{
[FieldDescription(nameof(FieldDescriptions.EventType))]
public EnrichedSchemaEventType Type { get; set; }
public DomainId Id

1
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedSchemaEventBase.cs

@ -11,6 +11,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public abstract class EnrichedSchemaEventBase : EnrichedUserEventBase
{
[FieldDescription(nameof(FieldDescriptions.EntityVersion))]
public NamedId<DomainId> SchemaId { get; set; }
}
}

2
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUsageExceededEvent.cs

@ -9,8 +9,10 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public sealed class EnrichedUsageExceededEvent : EnrichedEvent
{
[FieldDescription(nameof(FieldDescriptions.UsageCallsCurrent))]
public long CallsCurrent { get; set; }
[FieldDescription(nameof(FieldDescriptions.UsageCallsLimit))]
public long CallsLimit { get; set; }
public override long Partition

4
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedUserEventBase.cs

@ -10,14 +10,16 @@ using Squidex.Infrastructure;
using Squidex.Shared.Users;
#pragma warning disable CA1822 // Mark members as static
#pragma warning disable SA1133 // Do not combine attributes
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
{
public abstract class EnrichedUserEventBase : EnrichedEvent
{
[FieldDescription(nameof(FieldDescriptions.Actor))]
public RefToken Actor { get; set; }
[IgnoreDataMember]
[FieldDescription(nameof(FieldDescriptions.User)), IgnoreDataMember]
public IUser? User { get; set; }
public bool ShouldSerializeUser()

5
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -103,9 +103,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules
if (TryGetScript(text.Trim(), out var script))
{
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
["event"] = @event
Event = @event
};
#pragma warning disable MA0042 // Do not use blocking calls in an async method

2
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs

@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Core.Properties {
}
/// <summary>
/// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 403 (Forbidden)..
/// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest)..
/// </summary>
internal static string ScriptingDisallow {
get {

2
backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx

@ -142,7 +142,7 @@
<value>Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.</value>
</data>
<data name="ScriptingDisallow" xml:space="preserve">
<value>Tell Squidex to not allow the current operation and to return a 403 (Forbidden).</value>
<value>Tell Squidex to not allow the current operation and to return a 400 (BadRequest).</value>
</data>
<data name="ScriptingFormatDate" xml:space="preserve">
<value>Formats a JavaScript date object using the specified pattern.</value>

82
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetCommandScriptVars.cs

@ -0,0 +1,82 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Scripting.Internal;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class AssetCommandScriptVars : ScriptVars
{
[FieldDescription(nameof(FieldDescriptions.AssetParentId))]
public DomainId ParentId
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetFileHash))]
public string? FileHash
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetFileName))]
public string? FileName
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetSlug))]
public string? FileSlug
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetMimeType))]
public string? MimeType
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetParentPath))]
public Array? ParentPath
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetMetadata))]
public AssetMetadata? Metadata
{
set => SetValue(value != null ? new AssetMetadataWrapper(value) : null);
}
[FieldDescription(nameof(FieldDescriptions.AssetTags))]
public HashSet<string>? Tags
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetFileSize))]
public long FileSize
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetIsProtected))]
public bool? IsProtected
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))]
public bool? Permanent
{
set => SetValue(value);
}
}
}

83
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs

@ -0,0 +1,83 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class AssetEntityScriptVars : ScriptVars
{
[FieldDescription(nameof(FieldDescriptions.AssetParentId))]
public DomainId ParentId
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetFileHash))]
public string? FileHash
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetFileName))]
public string? FileName
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetSlug))]
public string? FileSlug
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetMimeType))]
public string? MimeType
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetParentPath))]
public Array? ParentPath
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetMetadata))]
public AssetMetadata? Metadata
{
set => SetValue(value != null ? new ReadOnlyDictionary<string, IJsonValue>(value) : null);
}
[FieldDescription(nameof(FieldDescriptions.AssetTags))]
public HashSet<string>? Tags
{
set => SetValue(value != null ? new ReadOnlyCollection<string>(value.ToList()) : null);
}
[FieldDescription(nameof(FieldDescriptions.AssetFileSize))]
public long FileSize
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetFileVersion))]
public long FileVersion
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AssetIsProtected))]
public bool? IsProtected
{
set => SetValue(value);
}
}
}

57
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs

@ -0,0 +1,57 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class AssetScriptVars : ScriptVars
{
[FieldDescription(nameof(FieldDescriptions.AppId))]
public DomainId AppId
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.EntityId))]
public DomainId AssetId
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AppName))]
public string AppName
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.Operation))]
public string Operation
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.Command))]
public AssetCommandScriptVars Command
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.Asset))]
public AssetEntityScriptVars Asset
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.User))]
public ClaimsPrincipal? User
{
set => SetValue(value);
}
}
}

101
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentScriptVars.cs

@ -0,0 +1,101 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ContentScriptVars : DataScriptVars
{
[FieldDescription(nameof(FieldDescriptions.AppId))]
public DomainId AppId
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.SchemaId))]
public DomainId SchemaId
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.EntityId))]
public DomainId ContentId
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.AppName))]
public string AppName
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.ContentSchemaName))]
public string SchemaName
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.Operation))]
public string Operation
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.EntityRequestDeletePermanent))]
public bool Permanent
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.User))]
public ClaimsPrincipal? User
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.ContentStatus))]
public Status Status
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.ContentStatusOld))]
public Status StatusOld
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.ContentStatusOld))]
public Status OldStatus
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.ContentData))]
public ContentData? DataOld
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.ContentDataOld))]
public ContentData? OldData
{
set => SetValue(value);
}
[FieldDescription(nameof(FieldDescriptions.ContentData))]
public override ContentData? Data
{
get => GetValue<ContentData?>();
set => SetValue(value);
}
}
}

20
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DataScriptVars.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public class DataScriptVars : ScriptVars
{
public virtual ContentData? Data
{
get => GetValue<ContentData?>();
set => SetValue(value);
}
}
}

19
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/EventScriptVars.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class EventScriptVars : ScriptVars
{
public EnrichedEvent Event
{
set => SetValue(value);
}
}
}

5
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -36,6 +36,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getJSON(url, callback, headers?)",
Resources.ScriptingGetJSON);

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
Task<IJsonValue> ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default,
CancellationToken ct = default);
Task<ContentData> TransformAsync(ScriptVars vars, string script, ScriptOptions options = default,
Task<ContentData> TransformAsync(DataScriptVars vars, string script, ScriptOptions options = default,
CancellationToken ct = default);
IJsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default);

125
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/AssetMetadataWrapper.cs

@ -0,0 +1,125 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting.Internal
{
internal sealed class AssetMetadataWrapper : IDictionary<string, object?>
{
private readonly AssetMetadata metadata;
public int Count
{
get => metadata.Count;
}
public ICollection<string> Keys
{
get => metadata.Keys;
}
public ICollection<object?> Values
{
get => metadata.Values.Cast<object?>().ToList();
}
public object? this[string key]
{
get => metadata[key];
set => metadata[key] = JsonValue.Create(value);
}
public bool IsReadOnly
{
get => false;
}
public AssetMetadataWrapper(AssetMetadata metadata)
{
this.metadata = metadata;
}
public bool TryGetValue(string key, [MaybeNullWhen(false)] out object? value)
{
if (metadata.TryGetValue(key, out var temp))
{
value = temp;
return true;
}
else
{
value = null;
return false;
}
}
public void Add(string key, object? value)
{
metadata.Add(key, JsonValue.Create(value));
}
public void Add(KeyValuePair<string, object?> item)
{
Add(item.Key, item.Value);
}
public bool Remove(string key)
{
return metadata.Remove(key);
}
public bool Remove(KeyValuePair<string, object?> item)
{
return false;
}
public void Clear()
{
metadata.Clear();
}
public bool Contains(KeyValuePair<string, object?> item)
{
return false;
}
public bool ContainsKey(string key)
{
return metadata.ContainsKey(key);
}
public void CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex)
{
var i = arrayIndex;
foreach (var item in metadata)
{
if (i >= array.Length)
{
break;
}
array[i] = new KeyValuePair<string, object?>(item.Key, item.Value);
i++;
}
}
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
{
return metadata.Select(x => new KeyValuePair<string, object?>(x.Key, x.Value)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)metadata).GetEnumerator();
}
}
}

15
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
}
public async Task<ContentData> TransformAsync(ScriptVars vars, string script, ScriptOptions options = default,
public async Task<ContentData> TransformAsync(DataScriptVars vars, string script, ScriptOptions options = default,
CancellationToken ct = default)
{
Guard.NotNull(vars);
@ -194,20 +194,25 @@ namespace Squidex.Domain.Apps.Core.Scripting
public void Describe(AddDescription describe, ScriptScope scope)
{
if (scope.HasFlag(ScriptScope.ContentTrigger) || scope.HasFlag(ScriptScope.AssetTrigger))
{
return;
}
if (scope.HasFlag(ScriptScope.Transform) || scope.HasFlag(ScriptScope.ContentScript))
{
describe(JsonType.Function, "replace()",
Resources.ScriptingReplace);
}
describe(JsonType.Function, "disallow()",
describe(JsonType.Function, "disallow(reason)",
Resources.ScriptingDisallow);
describe(JsonType.Function, "complete()",
Resources.ScriptingComplete);
describe(JsonType.Function, "reject(reason)",
Resources.ScriptingReject);
describe(JsonType.Function, "complete()",
Resources.ScriptingComplete);
}
}
}

5
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs

@ -17,11 +17,6 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
}
public ScriptContext(ScriptContext source)
: base(source, StringComparer.OrdinalIgnoreCase)
{
}
public bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value)
{
Guard.NotNull(key);

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptScope.cs

@ -10,10 +10,14 @@ namespace Squidex.Domain.Apps.Core.Scripting
[Flags]
public enum ScriptScope
{
Async,
AssetScript,
AssetTrigger,
ContentScript,
ContentTrigger,
Transform
Transform,
SchemaTrigger,
UsageTrigger,
CommentTrigger
}
}

18
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs

@ -6,27 +6,11 @@
// ==========================================================================
using System.Runtime.CompilerServices;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptVars : ScriptContext
public class ScriptVars : ScriptContext
{
public ScriptVars()
{
}
public ScriptVars(ScriptVars source)
: base(source)
{
}
public ContentData? Data
{
get => GetValue<ContentData?>();
set => SetValue(value);
}
public void SetValue(object? value, [CallerMemberName] string? key = null)
{
if (key != null)

331
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs

@ -5,170 +5,269 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections;
using System.Globalization;
using System.Reflection;
using System.Security.Claims;
using System.Text.RegularExpressions;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Reflection.Internal;
using Squidex.Shared.Users;
using Squidex.Text;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptingCompleter
{
private readonly IEnumerable<IScriptDescriptor> descriptors;
private static readonly FilterSchema DynamicData = new FilterSchema(FilterSchemaType.Object)
{
Fields = ReadonlyList.Create(
new FilterField(new FilterSchema(FilterSchemaType.Object), "my-field"),
new FilterField(FilterSchema.String, "my-field.iv"))
};
public ScriptingCompleter(IEnumerable<IScriptDescriptor> descriptors)
{
this.descriptors = descriptors;
}
public IReadOnlyList<ScriptingValue> Trigger(string type)
{
Guard.NotNull(type);
switch (type)
{
case "AssetChanged":
return AssetTrigger();
case "Comment":
return CommentTrigger();
case "ContentChanged":
return ContentTrigger(DynamicData);
case "SchemaChanged":
return SchemaTrigger();
case "Usage":
return UsageTrigger();
default:
return new List<ScriptingValue>();
}
}
public IReadOnlyList<ScriptingValue> ContentScript(FilterSchema dataSchema)
{
Guard.NotNull(dataSchema);
return new Process(descriptors).Content(dataSchema, ScriptScope.ContentScript | ScriptScope.Transform);
return new Process(descriptors, dataSchema.Flatten()).ContentScript();
}
public IReadOnlyList<ScriptingValue> ContentTrigger(FilterSchema dataSchema)
{
Guard.NotNull(dataSchema);
return new Process(descriptors).Content(dataSchema, ScriptScope.ContentTrigger);
return new Process(descriptors, dataSchema.Flatten()).ContentTrigger();
}
public IReadOnlyList<ScriptingValue> AssetScript()
{
return new Process(descriptors).Asset(ScriptScope.AssetScript);
return new Process(descriptors).AssetScript();
}
public IReadOnlyList<ScriptingValue> AssetTrigger()
{
return new Process(descriptors).Asset(ScriptScope.AssetTrigger);
return new Process(descriptors).AssetTrigger();
}
public IReadOnlyList<ScriptingValue> CommentTrigger()
{
return new Process(descriptors).CommentTrigger();
}
public IReadOnlyList<ScriptingValue> SchemaTrigger()
{
return new Process(descriptors).SchemaTrigger();
}
public IReadOnlyList<ScriptingValue> UsageTrigger()
{
return new Process(descriptors).UsageTrigger();
}
private sealed class Process
{
private static readonly Regex PropertyRegex = new Regex(@"^(?!\d)[\w$]+$", RegexOptions.Compiled);
private readonly Stack<string> prefixes = new Stack<string>();
private readonly HashSet<ScriptingValue> result = new HashSet<ScriptingValue>();
private readonly Dictionary<string, ScriptingValue> result = new Dictionary<string, ScriptingValue>();
private readonly IEnumerable<IScriptDescriptor> descriptors;
private readonly FilterSchema? dataSchema;
public Process(IEnumerable<IScriptDescriptor> descriptors)
public Process(IEnumerable<IScriptDescriptor> descriptors, FilterSchema? dataSchema = null)
{
this.descriptors = descriptors;
this.dataSchema = dataSchema;
}
public IReadOnlyList<ScriptingValue> Content(FilterSchema dataSchema, ScriptScope scope)
private IReadOnlyList<ScriptingValue> Build()
{
AddShared(scope);
return result.Values.OrderBy(x => x.Path).ToList();
}
AddObject("ctx", FieldDescriptions.Context, () =>
public IReadOnlyList<ScriptingValue> SchemaTrigger()
{
AddString("contentId",
FieldDescriptions.EntityId);
AddHelpers(ScriptScope.SchemaTrigger);
AddString("status",
FieldDescriptions.ContentStatus);
AddObject("event", FieldDescriptions.Event, () =>
{
AddType(typeof(EnrichedSchemaEvent));
});
AddString("statusOld",
FieldDescriptions.ContentStatusOld);
return Build();
}
AddObject("data", FieldDescriptions.ContentData, () =>
public IReadOnlyList<ScriptingValue> CommentTrigger()
{
AddData(dataSchema);
});
AddHelpers(ScriptScope.CommentTrigger);
AddObject("dataOld", FieldDescriptions.ContentDataOld, () =>
AddObject("event", FieldDescriptions.Event, () =>
{
AddData(dataSchema);
});
AddType(typeof(EnrichedCommentEvent));
});
return result.OrderBy(x => x.Path).ToList();
return Build();
}
public IReadOnlyList<ScriptingValue> Asset(ScriptScope scope)
public IReadOnlyList<ScriptingValue> UsageTrigger()
{
AddShared(scope);
AddHelpers(ScriptScope.UsageTrigger);
AddObject("ctx", FieldDescriptions.Context, () =>
AddObject("event", FieldDescriptions.Event, () =>
{
AddString("assetId",
FieldDescriptions.EntityId);
AddType(typeof(EnrichedUsageExceededEvent));
});
AddObject("asset",
FieldDescriptions.Asset, () =>
return Build();
}
public IReadOnlyList<ScriptingValue> ContentScript()
{
AddSharedAsset();
var scope = ScriptScope.ContentScript | ScriptScope.Transform | ScriptScope.Async;
AddNumber("fileVersion",
FieldDescriptions.AssetFileVersion);
});
AddHelpers(scope);
AddObject("command",
FieldDescriptions.Command, () =>
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddSharedAsset();
AddBoolean("permanent",
FieldDescriptions.EntityRequestDeletePermanent);
});
AddType(typeof(ContentScriptVars));
});
return result.OrderBy(x => x.Path).ToList();
return Build();
}
private void AddSharedAsset()
public IReadOnlyList<ScriptingValue> ContentTrigger()
{
AddString("fileHash",
FieldDescriptions.AssetFileHash);
AddString("fileName",
FieldDescriptions.AssetFileName);
var scope = ScriptScope.ContentTrigger;
AddString("fileSize",
FieldDescriptions.AssetFileSize);
AddHelpers(scope);
AddString("fileSlug",
FieldDescriptions.AssetSlug);
AddObject("event", FieldDescriptions.Event, () =>
{
AddType(typeof(EnrichedContentEvent));
});
AddString("mimeType",
FieldDescriptions.AssetMimeType);
return Build();
}
AddBoolean("isProtected",
FieldDescriptions.AssetIsProtected);
public IReadOnlyList<ScriptingValue> AssetTrigger()
{
AddHelpers(ScriptScope.AssetTrigger);
AddString("parentId",
FieldDescriptions.AssetParentId);
AddObject("event", FieldDescriptions.Event, () =>
{
AddType(typeof(EnrichedAssetEvent));
});
AddArray("parentPath",
FieldDescriptions.AssetParentPath);
return Build();
}
AddArray("tags",
FieldDescriptions.AssetTags);
public IReadOnlyList<ScriptingValue> AssetScript()
{
AddHelpers(ScriptScope.AssetScript | ScriptScope.Async);
AddObject("metadata",
FieldDescriptions.AssetMetadata, () =>
AddObject("ctx", FieldDescriptions.Event, () =>
{
AddArray("name",
FieldDescriptions.AssetMetadataValue);
AddType(typeof(AssetScriptVars));
});
return Build();
}
private void AddShared(ScriptScope scope)
private void AddHelpers(ScriptScope scope)
{
foreach (var descriptor in descriptors)
{
descriptor.Describe(Add, scope);
}
}
AddString("appId",
FieldDescriptions.AppId);
private void AddType(Type type)
{
foreach (var (name, description, propertyTypeOrNullable) in GetFields(type))
{
var propertyType = Nullable.GetUnderlyingType(propertyTypeOrNullable) ?? propertyTypeOrNullable;
AddString("appName",
FieldDescriptions.AppName);
if (propertyType.IsEnum ||
propertyType == typeof(string) ||
propertyType == typeof(DomainId) ||
propertyType == typeof(Instant) ||
propertyType == typeof(Status))
{
AddString(name, description);
}
else if (propertyType == typeof(int) || propertyType == typeof(long))
{
AddNumber(name, description);
}
else if (propertyType == typeof(bool))
{
AddBoolean(name, description);
}
else if (propertyType == typeof(AssetMetadata))
{
AddObject(name, description, () =>
{
AddString("my-name",
FieldDescriptions.AssetMetadataValue);
});
}
else if (propertyType == typeof(NamedId<DomainId>))
{
AddObject(name, description, () =>
{
AddString("id",
FieldDescriptions.NamedId);
AddString("operation",
FieldDescriptions.Operation);
AddString("name",
FieldDescriptions.NamedName);
});
}
else if (propertyType == typeof(RefToken))
{
AddObject(name, description, () =>
{
AddString("identifier",
FieldDescriptions.ActorIdentifier);
AddObject("user",
FieldDescriptions.User, () =>
AddString("type",
FieldDescriptions.ActorType);
});
}
else if (propertyType == typeof(IUser) || propertyType == typeof(ClaimsPrincipal))
{
AddObject(name, description, () =>
{
AddString("id",
FieldDescriptions.UserId);
@ -182,18 +281,54 @@ namespace Squidex.Domain.Apps.Core.Scripting
AddBoolean("isUser",
FieldDescriptions.UserIsUser);
AddObject("claims",
FieldDescriptions.UserClaims, () =>
AddObject("claims", FieldDescriptions.UserClaims, () =>
{
AddArray("name",
FieldDescriptions.UsersClaimsValue);
});
});
}
else if (propertyType == typeof(ContentData) && dataSchema != null)
{
AddObject(name, description, () =>
{
AddData();
});
}
else if (GetFields(propertyType).Any())
{
AddObject(name, description, () =>
{
AddType(propertyType);
});
}
else if (propertyType.GetInterfaces().Contains(typeof(IEnumerable)))
{
AddArray(name, description);
}
}
}
private void AddData(FilterSchema dataSchema)
private static IEnumerable<(string Name, string Description, Type Type)> GetFields(Type type)
{
foreach (var property in type.GetPublicProperties())
{
if (dataSchema.Fields == null)
var descriptionKey = property.GetCustomAttribute<FieldDescriptionAttribute>()?.Name;
if (descriptionKey == null)
{
continue;
}
var description = FieldDescriptions.ResourceManager.GetString(descriptionKey, CultureInfo.InvariantCulture)!;
yield return (property.Name.ToCamelCase(), description, property.PropertyType);
}
}
private void AddData()
{
if (dataSchema?.Fields == null)
{
return;
}
@ -268,9 +403,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
private void Add(JsonType type, string? name, string? description)
{
if (name != null)
var parts = name?.Split('.') ?? Array.Empty<string>();
foreach (var part in parts)
{
prefixes.Push(name);
PushPrefix(part);
}
if (prefixes.Count == 0)
@ -278,11 +415,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
return;
}
var path = string.Join('.', prefixes.Reverse());
var path = string.Concat(prefixes.Reverse());
result.Add(new ScriptingValue(path, type, description));
result[path] = new ScriptingValue(path, type, description);
if (name != null)
for (int i = 0; i < parts.Length; i++)
{
prefixes.Pop();
}
@ -292,16 +429,36 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
Add(JsonType.Object, name, description);
prefixes.Push(name);
try
var parts = name.Split('.');
foreach (var part in parts)
{
inner();
PushPrefix(part);
}
finally
inner();
for (int i = 0; i < parts.Length; i++)
{
prefixes.Pop();
}
}
private void PushPrefix(string name)
{
if (prefixes.Count == 0)
{
prefixes.Push(name);
}
else if (PropertyRegex.IsMatch(name))
{
prefixes.Push($".{name}");
}
else
{
prefixes.Push($"['{name}']");
}
}
}
}
}

9
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -56,6 +56,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
SimpleMapper.Map(asset, result);
result.Actor = asset.LastModifiedBy;
result.PixelHeight = asset.Metadata?.GetPixelHeight();
result.PixelWidth = asset.Metadata?.GetPixelWidth();
result.Name = "AssetQueried";
yield return result;
@ -78,6 +80,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
SimpleMapper.Map(asset, result);
result.PixelHeight = asset.Metadata?.GetPixelHeight();
result.PixelWidth = asset.Metadata?.GetPixelWidth();
result.AssetType = asset.Type;
}
@ -109,9 +113,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
return true;
}
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
["event"] = @event
Event = @event
};
return scriptEngine.Evaluate(vars, trigger.Condition);

5
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs

@ -42,6 +42,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getAssets(ids, callback)",
Resources.ScriptingGetAssets);

198
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs

@ -5,12 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
@ -24,29 +21,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
CanReject = true
};
private static class ScriptKeys
{
public const string AppId = "appId";
public const string AppName = "appName";
public const string Asset = "asset";
public const string AssetId = "assetId";
public const string Command = "command";
public const string FileHash = "fileHash";
public const string FileName = "fileName";
public const string FileSize = "fileSize";
public const string FileSlug = "fileSlug";
public const string FileVersion = "fileVersion";
public const string IsProtected = "isProtected";
public const string Metadata = "metadata";
public const string MimeType = "mimeType";
public const string Operation = "operation";
public const string ParentId = "parentId";
public const string ParentPath = "parentPath";
public const string Permanent = "permanent";
public const string Tags = "tags";
public const string User = "User";
}
public static async Task ExecuteCreateScriptAsync(this AssetOperation operation, CreateAsset create)
{
var script = operation.App.AssetScripts?.Create;
@ -58,23 +32,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
var parentPath = await GetPathAsync(operation, create.ParentId);
// Tags and metadata are mutable and can be changed from the scripts, but not replaced.
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new AssetScriptVars
{
// Use a dictionary for better performance, because no reflection is involved.
[ScriptKeys.Command] = new Dictionary<string, object?>
{
[ScriptKeys.Metadata] = create.Metadata.Mutable(),
[ScriptKeys.FileHash] = create.FileHash,
[ScriptKeys.FileName] = create.File.FileName,
[ScriptKeys.FileSize] = create.File.FileSize,
[ScriptKeys.FileSlug] = create.File.FileName.Slugify(),
[ScriptKeys.MimeType] = create.File.MimeType,
[ScriptKeys.ParentId] = create.ParentId,
[ScriptKeys.ParentPath] = parentPath,
[ScriptKeys.Tags] = create.Tags
// Tags and metadata are mutable and can be changed from the scripts, but not replaced.
Command = new AssetCommandScriptVars
{
FileHash = create.FileHash,
FileName = create.File.FileName,
FileSlug = create.File.FileName.Slugify(),
FileSize = create.File.FileSize,
Metadata = create.Metadata,
MimeType = create.File.MimeType,
ParentId = create.ParentId,
ParentPath = parentPath,
Tags = create.Tags
},
[ScriptKeys.Operation] = "Create"
Operation = "Create"
};
await ExecuteScriptAsync(operation, script, vars);
@ -89,20 +63,20 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
return Task.CompletedTask;
}
// Tags and metadata are mutable and can be changed from the scripts, but not replaced.
var vars = new ScriptVars
{
// Use a dictionary for better performance, because no reflection is involved.
[ScriptKeys.Command] = new Dictionary<string, object?>
// Script vars are just wrappers over dictionaries for better performance.
var vars = new AssetScriptVars
{
[ScriptKeys.Metadata] = update.Metadata.Mutable(),
[ScriptKeys.FileHash] = update.FileHash,
[ScriptKeys.FileName] = update.File.FileName,
[ScriptKeys.FileSize] = update.File.FileSize,
[ScriptKeys.MimeType] = update.File.MimeType,
[ScriptKeys.Tags] = update.Tags
// Tags and metadata are mutable and can be changed from the scripts, but not replaced.
Command = new AssetCommandScriptVars
{
Metadata = update.Metadata,
FileHash = update.FileHash,
FileName = update.File.FileName,
FileSize = update.File.FileSize,
MimeType = update.File.MimeType,
Tags = update.Tags
},
[ScriptKeys.Operation] = "Update"
Operation = "Update"
};
return ExecuteScriptAsync(operation, script, vars);
@ -117,18 +91,19 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
return Task.CompletedTask;
}
// Tags are mutable and can be changed from the scripts, but not replaced.
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new AssetScriptVars
{
// Use a dictionary for better performance, because no reflection is involved.
[ScriptKeys.Command] = new Dictionary<string, object?>
// Tags are mutable and can be changed from the scripts, but not replaced.
Command = new AssetCommandScriptVars
{
[ScriptKeys.Metadata] = annotate.Metadata?.Mutable(),
[ScriptKeys.FileName] = annotate.FileName,
[ScriptKeys.FileSlug] = annotate.Slug,
[ScriptKeys.Tags] = annotate.Tags
IsProtected = annotate.IsProtected,
Metadata = annotate.Metadata,
FileName = annotate.FileName,
FileSlug = annotate.Slug,
Tags = annotate.Tags
},
[ScriptKeys.Operation] = "Annotate"
Operation = "Annotate"
};
return ExecuteScriptAsync(operation, script, vars);
@ -145,15 +120,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
var parentPath = await GetPathAsync(operation, move.ParentId);
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new AssetScriptVars
{
// Use a dictionary for better performance, because no reflection is involved.
[ScriptKeys.Command] = new Dictionary<string, object?>
Command = new AssetCommandScriptVars
{
[ScriptKeys.ParentId] = move.ParentId,
[ScriptKeys.ParentPath] = parentPath
ParentId = move.ParentId,
ParentPath = parentPath
},
[ScriptKeys.Operation] = "Move"
Operation = "Move"
};
await ExecuteScriptAsync(operation, script, vars);
@ -168,53 +143,53 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
return Task.CompletedTask;
}
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new AssetScriptVars
{
// Use a dictionary for better performance, because no reflection is involved.
[ScriptKeys.Command] = new Dictionary<string, object?>
Command = new AssetCommandScriptVars
{
[ScriptKeys.Permanent] = delete.Permanent
Permanent = delete.Permanent
},
[ScriptKeys.Operation] = "Delete"
Operation = "Delete"
};
return ExecuteScriptAsync(operation, script, vars);
}
private static async Task ExecuteScriptAsync(AssetOperation operation, string script, ScriptVars vars)
private static async Task ExecuteScriptAsync(AssetOperation operation, string script, AssetScriptVars vars)
{
var snapshot = operation.Snapshot;
var parentPath = await GetPathAsync(operation, snapshot.ParentId);
// Use a dictionary for better performance, because no reflection is involved.
var asset = new Dictionary<string, object?>
{
[ScriptKeys.Metadata] = snapshot.ReadonlyMetadata(),
[ScriptKeys.FileHash] = snapshot.FileHash,
[ScriptKeys.FileName] = snapshot.FileName,
[ScriptKeys.FileSize] = snapshot.FileSize,
[ScriptKeys.FileSlug] = snapshot.Slug,
[ScriptKeys.FileVersion] = snapshot.FileVersion,
[ScriptKeys.IsProtected] = snapshot.IsProtected,
[ScriptKeys.MimeType] = snapshot.MimeType,
[ScriptKeys.ParentId] = snapshot.ParentId,
[ScriptKeys.ParentPath] = parentPath,
[ScriptKeys.Tags] = snapshot.ReadonlyTags()
// Script vars are just wrappers over dictionaries for better performance.
var asset = new AssetEntityScriptVars
{
Metadata = snapshot.Metadata,
FileHash = snapshot.FileHash,
FileName = snapshot.FileName,
FileSize = snapshot.FileSize,
FileSlug = snapshot.Slug,
FileVersion = snapshot.FileVersion,
IsProtected = snapshot.IsProtected,
MimeType = snapshot.MimeType,
ParentId = snapshot.ParentId,
ParentPath = parentPath,
Tags = snapshot.Tags
};
vars[ScriptKeys.AppId] = operation.App.Id;
vars[ScriptKeys.AppName] = operation.App.Name;
vars[ScriptKeys.AssetId] = operation.CommandId;
vars[ScriptKeys.Asset] = asset;
vars[ScriptKeys.User] = operation.User;
vars.AppId = operation.App.Id;
vars.AppName = operation.App.Name;
vars.AssetId = operation.CommandId;
vars.Asset = asset;
vars.User = operation.User;
var scriptEngine = operation.Resolve<IScriptEngine>();
await scriptEngine.ExecuteAsync(vars, script, Options);
}
private static async Task<object> GetPathAsync(AssetOperation operation, DomainId parentId)
private static async Task<Array> GetPathAsync(AssetOperation operation, DomainId parentId)
{
if (parentId == default)
{
@ -222,40 +197,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
}
var assetQuery = operation.Resolve<IAssetQueryService>();
var assetPath = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId);
var path = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId);
return path.Select(x => new { id = x.Id, folderName = x.FolderName }).ToList();
}
private static object? Mutable(this AssetMetadata metadata)
{
if (metadata == null)
{
return null;
}
return new ScriptMetadataWrapper(metadata);
}
private static object? ReadonlyMetadata(this IAssetEntity asset)
{
if (asset.Metadata == null)
{
return null;
}
return new ReadOnlyDictionary<string, IJsonValue>(asset.Metadata);
}
private static object? ReadonlyTags(this IAssetEntity asset)
{
if (asset.Tags == null)
{
return null;
}
return new ReadOnlyCollection<string>(asset.Tags.ToList());
return assetPath.Select(x => new { id = x.Id, folderName = x.FolderName }).ToArray();
}
}
}

5
backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs

@ -74,9 +74,10 @@ namespace Squidex.Domain.Apps.Entities.Comments
return true;
}
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
["event"] = @event
Event = @event
};
return scriptEngine.Evaluate(vars, trigger.Condition);

5
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -215,9 +215,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
return true;
}
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
["event"] = @event
Event = @event
};
return scriptEngine.Evaluate(vars, schema.Condition);

2
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -413,7 +413,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
if (!c.DoNotScript)
{
await operation.ExecuteDeleteScriptAsync();
await operation.ExecuteDeleteScriptAsync(c.Permanent);
}
if (c.CheckReferrers)

115
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs

@ -19,25 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
CanReject = true
};
private static class ScriptKeys
{
public const string AppId = "appId";
public const string AppName = "appName";
public const string Command = "command";
public const string Content = "content";
public const string ContentId = "contentId";
public const string Data = "data";
public const string DataOld = "dataOld";
public const string OldData = "oldData";
public const string OldStatus = "oldStatus";
public const string Operation = "operation";
public const string SchemaId = "achemaId";
public const string SchemaName = "achemaName";
public const string Status = "status";
public const string StatusOld = "statusOld";
public const string User = "user";
}
public static Task<ContentData> ExecuteCreateScriptAsync(this ContentOperation operation, ContentData data, Status status)
{
var script = operation.SchemaDef.Scripts.Create;
@ -47,15 +28,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
return Task.FromResult(data);
}
var vars = Enrich(operation, new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = Enrich(operation, new ContentScriptVars
{
[ScriptKeys.Data] = data,
[ScriptKeys.DataOld] = null,
[ScriptKeys.OldData] = null,
[ScriptKeys.OldStatus] = default(Status),
[ScriptKeys.Operation] = "Create",
[ScriptKeys.Status] = status,
[ScriptKeys.StatusOld] = default(Status)
Data = data,
DataOld = null,
OldData = null,
OldStatus = default,
Operation = "Create",
Status = status,
StatusOld = default
});
return TransformAsync(operation, script, vars);
@ -70,15 +52,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
return Task.FromResult(data);
}
var vars = Enrich(operation, new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = Enrich(operation, new ContentScriptVars
{
[ScriptKeys.Data] = data,
[ScriptKeys.DataOld] = operation.Snapshot.Data,
[ScriptKeys.OldData] = operation.Snapshot.Data,
[ScriptKeys.OldStatus] = data,
[ScriptKeys.Operation] = "Update",
[ScriptKeys.Status] = operation.Snapshot.EditingStatus(),
[ScriptKeys.StatusOld] = default(Status)
Data = data,
DataOld = operation.Snapshot.Data,
OldData = operation.Snapshot.Data,
OldStatus = operation.Snapshot.Status,
Operation = "Update",
Status = operation.Snapshot.EditingStatus(),
StatusOld = default
});
return TransformAsync(operation, script, vars);
@ -93,24 +76,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
return Task.FromResult(operation.Snapshot.Data);
}
// Clone the data so we do not change it by accident.
var data = operation.Snapshot.Data.Clone();
var vars = Enrich(operation, new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = Enrich(operation, new ContentScriptVars
{
[ScriptKeys.Data] = data,
[ScriptKeys.DataOld] = null,
[ScriptKeys.OldData] = null,
[ScriptKeys.OldStatus] = operation.Snapshot.EditingStatus(),
[ScriptKeys.Operation] = change.ToString(),
[ScriptKeys.Status] = status,
[ScriptKeys.StatusOld] = operation.Snapshot.EditingStatus()
Data = operation.Snapshot.Data.Clone(),
DataOld = null,
OldData = null,
OldStatus = operation.Snapshot.EditingStatus(),
Operation = change.ToString(),
Status = status,
StatusOld = operation.Snapshot.EditingStatus()
});
return TransformAsync(operation, script, vars);
}
public static Task ExecuteDeleteScriptAsync(this ContentOperation operation)
public static Task ExecuteDeleteScriptAsync(this ContentOperation operation, bool permanent)
{
var script = operation.SchemaDef.Scripts.Delete;
@ -119,40 +100,40 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
return Task.CompletedTask;
}
var vars = Enrich(operation, new ScriptVars
{
[ScriptKeys.Data] = operation.Snapshot.Data,
[ScriptKeys.DataOld] = null,
[ScriptKeys.OldData] = null,
[ScriptKeys.OldStatus] = operation.Snapshot.EditingStatus(),
[ScriptKeys.Operation] = "Delete",
[ScriptKeys.Status] = operation.Snapshot.EditingStatus(),
[ScriptKeys.StatusOld] = default(Status)
// Script vars are just wrappers over dictionaries for better performance.
var vars = Enrich(operation, new ContentScriptVars
{
Data = operation.Snapshot.Data,
DataOld = null,
OldData = null,
OldStatus = operation.Snapshot.EditingStatus(),
Operation = "Delete",
Permanent = permanent,
Status = operation.Snapshot.EditingStatus(),
StatusOld = default
});
return ExecuteAsync(operation, script, vars);
}
private static async Task<ContentData> TransformAsync(ContentOperation operation, string script, ScriptVars vars)
private static async Task<ContentData> TransformAsync(ContentOperation operation, string script, ContentScriptVars vars)
{
return await operation.Resolve<IScriptEngine>().TransformAsync(vars, script, Options);
}
private static async Task ExecuteAsync(ContentOperation operation, string script, ScriptVars vars)
private static async Task ExecuteAsync(ContentOperation operation, string script, ContentScriptVars vars)
{
await operation.Resolve<IScriptEngine>().ExecuteAsync(vars, script, Options);
}
private static ScriptVars Enrich(ContentOperation operation, ScriptVars vars)
private static ContentScriptVars Enrich(ContentOperation operation, ContentScriptVars vars)
{
vars[ScriptKeys.AppId] = operation.App.Id;
vars[ScriptKeys.AppName] = operation.App.Name;
vars[ScriptKeys.Command] = operation.Command;
vars[ScriptKeys.Content] = operation.Snapshot;
vars[ScriptKeys.ContentId] = operation.CommandId;
vars[ScriptKeys.SchemaId] = operation.Schema.Id;
vars[ScriptKeys.SchemaName] = operation.Schema.SchemaDef.Name;
vars[ScriptKeys.User] = operation.User;
vars.AppId = operation.App.Id;
vars.AppName = operation.App.Name;
vars.ContentId = operation.CommandId;
vars.SchemaId = operation.Schema.Id;
vars.SchemaName = operation.Schema.SchemaDef.Name;
vars.User = operation.User;
return vars;
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs

@ -115,9 +115,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!string.IsNullOrWhiteSpace(condition?.Expression) && data != null)
{
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = data
Data = data
};
return scriptEngine.Evaluate(vars, condition.Expression);

39
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs

@ -14,16 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
private readonly IScriptEngine scriptEngine;
private static class ScriptKeys
{
public const string AppId = "appId";
public const string AppName = "appName";
public const string ContentId = "contentId";
public const string Data = "data";
public const string Operation = "operation";
public const string User = "user";
}
public ScriptContent(IScriptEngine scriptEngine)
{
this.scriptEngine = scriptEngine;
@ -48,11 +38,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
continue;
}
var vars = new ScriptVars
var vars = new ContentScriptVars
{
[ScriptKeys.AppId] = context.App.Id,
[ScriptKeys.AppName] = context.App.Name,
[ScriptKeys.User] = context.User
AppId = schema.AppId.Id,
AppName = schema.AppId.Name,
SchemaId = schema.Id,
SchemaName = schema.SchemaDef.Name,
User = context.User
};
var preScript = schema.SchemaDef.Scripts.QueryPre;
@ -74,15 +66,26 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
}
}
private async Task TransformAsync(ScriptVars rootVars, string script, ContentEntity content,
private async Task TransformAsync(ContentScriptVars sharedVars, string script, ContentEntity content,
CancellationToken ct)
{
var vars = new ScriptVars(rootVars)
var vars = new ContentScriptVars
{
[ScriptKeys.ContentId] = content.Id,
[ScriptKeys.Data] = content.Data,
ContentId = content.Id,
Data = content.Data,
DataOld = default,
Status = content.Status,
StatusOld = default
};
foreach (var (key, value) in sharedVars)
{
if (!vars.ContainsKey(key))
{
vars[key] = value;
}
}
var options = new ScriptOptions
{
AsContext = true

5
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs

@ -49,6 +49,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
public void Describe(AddDescription describe, ScriptScope scope)
{
if (!scope.HasFlag(ScriptScope.Async))
{
return;
}
describe(JsonType.Function, "getReferences(ids, callback)",
Resources.ScriptingGetReferences);

3
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs

@ -79,7 +79,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas
return true;
}
var vars = new ScriptVars
// Script vars are just wrappers over dictionaries for better performance.
var vars = new EventScriptVars
{
["event"] = @event
};

10
backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs

@ -30,7 +30,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </summary>
/// <param name="app">The name of the app to get the asset scripts for.</param>
/// <returns>
/// 200 => App asset scripts returned.
/// 200 => Asset scripts returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
@ -38,7 +38,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppAssetSScriptsRead)]
[ApiCosts(0)]
public IActionResult GetScripts(string app)
public IActionResult GetAssetScripts(string app)
{
var response = Deferred.Response(() =>
{
@ -54,8 +54,8 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <param name="app">The name of the app to update.</param>
/// <param name="request">The values to update.</param>
/// <returns>
/// 200 => App updated.
/// 400 => App request not valid.
/// 200 => Asset scripts updated.
/// 400 => Asset request not valid.
/// 404 => App not found.
/// </returns>
[HttpPut]
@ -63,7 +63,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppAssetsScriptsUpdate)]
[ApiCosts(0)]
public async Task<IActionResult> PutScripts(string app, [FromBody] UpdateAssetScriptsDto request)
public async Task<IActionResult> PutAssetScripts(string app, [FromBody] UpdateAssetScriptsDto request)
{
var response = await InvokeCommandAsync(request.ToCommand());

2
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -225,7 +225,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.IsAllowed(Shared.Permissions.AppAssetsScriptsUpdate, Name, additional: permissions))
{
AddDeleteLink("assets/scripts", resources.Url<AppAssetsController>(x => nameof(x.GetScripts), values));
AddDeleteLink("assets/scripts", resources.Url<AppAssetsController>(x => nameof(x.GetAssetScripts), values));
}
AddGetLink("settings", resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values));

2
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs

@ -58,7 +58,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
if (resources.CanUpdateAssetsScripts)
{
AddPutLink("update", resources.Url<AppAssetsController>(x => nameof(x.PutScripts), values));
AddPutLink("update", resources.Url<AppAssetsController>(x => nameof(x.PutAssetScripts), values));
}
return this;

8
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -425,9 +425,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public IActionResult GetScriptCompletion(string app, string schema)
public IActionResult GetScriptCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer)
{
var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.AssetScript();
return Ok(completion);
@ -438,9 +438,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public IActionResult GetScriptTriggerCompletion(string app, string schema)
public IActionResult GetScriptTriggerCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer)
{
var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.AssetTrigger();
return Ok(completion);

1
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -6,7 +6,6 @@
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;

15
backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -9,8 +9,10 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using NodaTime;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
@ -469,6 +471,19 @@ namespace Squidex.Areas.Api.Controllers.Rules
return Content(schema.ToJson(), "application/json");
}
[HttpGet]
[Route("apps/{app}/rules/completion/{triggerType}")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public IActionResult GetScriptCompletion(string app, string triggerType,
[FromServices] ScriptingCompleter completer)
{
var completion = completer.Trigger(triggerType);
return Ok(completion);
}
private async Task<RuleDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);

10
backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -335,9 +335,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public async Task<IActionResult> GetScriptCompletion(string app, string schema)
public async Task<IActionResult> GetScriptCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer)
{
var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.ContentScript(await BuildModel());
return Ok(completion);
@ -348,9 +348,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public async Task<IActionResult> GetScriptTriggerCompletion(string app, string schema)
public async Task<IActionResult> GetScriptTriggerCompletion(string app, string schema,
[FromServices] ScriptingCompleter completer)
{
var completer = HttpContext.RequestServices.GetRequiredService<ScriptingCompleter>();
var completion = completer.ContentTrigger(await BuildModel());
return Ok(completion);
@ -374,7 +374,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
{
var components = await appProvider.GetComponentsAsync(Schema, HttpContext.RequestAborted);
return Schema.SchemaDef.BuildDataSchema(App.PartitionResolver(), components).Flatten();
return Schema.SchemaDef.BuildDataSchema(App.PartitionResolver(), components);
}
private Task<ISchemaEntity?> GetSchemaAsync(string schema)

2
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -67,7 +67,7 @@ namespace Squidex.Config.Domain
.AsSelf();
services.AddSingletonAs<JintScriptEngine>()
.As<IScriptEngine>();
.As<IScriptEngine>().As<IScriptDescriptor>();
services.AddSingletonAs<GrainTagService>()
.As<ITagService>();

1
backend/src/Squidex/Config/Domain/QueryServices.cs

@ -7,7 +7,6 @@
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Web.Services;

38
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
var content = new ContentData();
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = content
};
@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(10.0));
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = content
};
@ -150,13 +150,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
throw 'Error';
";
await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(new ScriptVars(), script));
await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(new DataScriptVars(), script));
}
[Fact]
public async Task TransformAsync_should_throw_exception_if_script_failed()
{
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = new ContentData()
};
@ -171,7 +171,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_return_original_content_if_not_replaced()
{
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = new ContentData()
};
@ -188,7 +188,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_return_original_content_if_not_replaced_async()
{
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = new ContentData()
};
@ -217,7 +217,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant("MyOperation"));
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = content,
["dataOld"] = null,
@ -248,7 +248,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(42));
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = content,
["dataOld"] = null,
@ -274,7 +274,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_not_ignore_transformation_if_async_not_set()
{
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = new ContentData(),
["dataOld"] = null,
@ -300,7 +300,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task TransformAsync_should_not_timeout_if_replace_never_called()
{
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = new ContentData(),
["dataOld"] = null,
@ -338,7 +338,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(10.0));
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = content
};
@ -385,7 +385,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2"));
var vars = new ScriptVars
var vars = new DataScriptVars
{
["data"] = content,
["dataOld"] = oldContent,
@ -539,7 +539,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
[Fact]
public async Task Should_share_vars_between_execution_for_transform()
{
var vars = new ScriptVars
var vars = new DataScriptVars
{
["value"] = 13
};
@ -557,11 +557,19 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
sut.Execute(vars, script1, new ScriptOptions { AsContext = true });
#pragma warning restore MA0042 // Do not use blocking calls in an async method
var vars2 = new ScriptVars(vars)
var vars2 = new DataScriptVars()
{
Data = new ContentData()
["data"] = new ContentData()
};
foreach (var (key, value) in vars)
{
if (!vars2.ContainsKey(key))
{
vars2[key] = value;
}
}
var result = await sut.TransformAsync(vars2, script2, new ScriptOptions { AsContext = true });
Assert.Equal(JsonValue.Create(28), result["test"]!["iv"]);

380
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs

@ -0,0 +1,380 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using FakeItEasy;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.GenerateFilters;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure.Queries;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
public class ScriptingCompleterTests
{
private readonly IScriptDescriptor scriptDescriptor1 = A.Fake<IScriptDescriptor>();
private readonly IScriptDescriptor scriptDescriptor2 = A.Fake<IScriptDescriptor>();
private readonly FilterSchema dataSchema;
private readonly ScriptingCompleter sut;
public ScriptingCompleterTests()
{
var schema =
new Schema("simple")
.AddString(1, "my-field", Partitioning.Invariant);
dataSchema = schema.BuildDataSchema(LanguagesConfig.English.ToResolver(), ResolvedComponents.Empty);
sut = new ScriptingCompleter(new[] { scriptDescriptor1, scriptDescriptor2 });
}
[Fact]
public void Should_calls_descriptors()
{
sut.UsageTrigger();
A.CallTo(() => scriptDescriptor1.Describe(A<AddDescription>._, A<ScriptScope>._))
.MustHaveHappened();
A.CallTo(() => scriptDescriptor2.Describe(A<AddDescription>._, A<ScriptScope>._))
.MustHaveHappened();
}
[Fact]
public void Should_describe_content_script()
{
var result = sut.ContentScript(dataSchema);
AssertCompletion(result,
PresetUser("ctx.user"),
new[]
{
"ctx",
"ctx.appId",
"ctx.appName",
"ctx.contentId",
"ctx.data",
"ctx.data['my-field']",
"ctx.data['my-field'].iv",
"ctx.dataOld",
"ctx.dataOld['my-field']",
"ctx.dataOld['my-field'].iv",
"ctx.oldData",
"ctx.oldData['my-field']",
"ctx.oldData['my-field'].iv",
"ctx.oldStatus",
"ctx.operation",
"ctx.permanent",
"ctx.schemaId",
"ctx.schemaName",
"ctx.status",
"ctx.statusOld"
});
}
[Fact]
public void Should_describe_asset_script()
{
var result = sut.AssetScript();
AssertCompletion(result,
PresetUser("ctx.user"),
new[]
{
"ctx",
"ctx.appId",
"ctx.appName",
"ctx.asset",
"ctx.asset.fileHash",
"ctx.asset.fileName",
"ctx.asset.fileSize",
"ctx.asset.fileSlug",
"ctx.asset.fileVersion",
"ctx.asset.isProtected",
"ctx.asset.metadata",
"ctx.asset.metadata['my-name']",
"ctx.asset.mimeType",
"ctx.asset.parentId",
"ctx.asset.parentPath",
"ctx.asset.tags",
"ctx.assetId",
"ctx.command",
"ctx.command.fileHash",
"ctx.command.fileName",
"ctx.command.fileSize",
"ctx.command.fileSlug",
"ctx.command.isProtected",
"ctx.command.metadata",
"ctx.command.metadata['my-name']",
"ctx.command.mimeType",
"ctx.command.parentId",
"ctx.command.parentPath",
"ctx.command.permanent",
"ctx.command.tags",
"ctx.operation",
});
}
[Fact]
public void Should_describe_content_trigger()
{
var result = sut.ContentTrigger(dataSchema);
AssertContentTrigger(result);
}
[Fact]
public void Should_describe_dynamic_content_trigger()
{
var result = sut.Trigger("ContentChanged");
AssertContentTrigger(result);
}
private static void AssertContentTrigger(IReadOnlyList<ScriptingValue> result)
{
AssertCompletion(result,
PresetActor("event.actor"),
PresetUser("event.user"),
new[]
{
"event",
"event.appId",
"event.appId.id",
"event.appId.name",
"event.created",
"event.createdBy",
"event.createdBy.identifier",
"event.createdBy.type",
"event.data",
"event.data['my-field']",
"event.data['my-field'].iv",
"event.dataOld",
"event.dataOld['my-field']",
"event.dataOld['my-field'].iv",
"event.lastModified",
"event.lastModifiedBy",
"event.lastModifiedBy.identifier",
"event.lastModifiedBy.type",
"event.id",
"event.name",
"event.newStatus",
"event.schemaId",
"event.schemaId.id",
"event.schemaId.name",
"event.status",
"event.timestamp",
"event.type",
"event.version"
});
}
[Fact]
public void Should_describe_asset_trigger()
{
var result = sut.AssetTrigger();
AssertAssetTrigger(result);
}
[Fact]
public void Should_describe_dynamicasset_trigger()
{
var result = sut.Trigger("AssetChanged");
AssertAssetTrigger(result);
}
private void AssertAssetTrigger(IReadOnlyList<ScriptingValue> result)
{
AssertCompletion(result,
PresetActor("event.actor"),
PresetUser("event.user"),
new[]
{
"event",
"event.appId",
"event.appId.id",
"event.appId.name",
"event.assetType",
"event.created",
"event.createdBy",
"event.createdBy.identifier",
"event.createdBy.type",
"event.fileHash",
"event.fileName",
"event.fileSize",
"event.fileVersion",
"event.isImage",
"event.isProtected",
"event.lastModified",
"event.lastModifiedBy",
"event.lastModifiedBy.identifier",
"event.lastModifiedBy.type",
"event.id",
"event.metadata",
"event.metadata['my-name']",
"event.mimeType",
"event.name",
"event.parentId",
"event.pixelHeight",
"event.pixelWidth",
"event.slug",
"event.timestamp",
"event.type",
"event.version"
});
}
[Fact]
public void Should_describe_comment_trigger()
{
var result = sut.CommentTrigger();
AssertCommentTrigger(result);
}
[Fact]
public void Should_describe_dynamic_comment_trigger()
{
var result = sut.Trigger("Comment");
AssertCommentTrigger(result);
}
private static void AssertCommentTrigger(IReadOnlyList<ScriptingValue> result)
{
AssertCompletion(result,
PresetActor("event.actor"),
PresetUser("event.user"),
PresetUser("event.mentionedUser"),
new[]
{
"event",
"event.appId",
"event.appId.id",
"event.appId.name",
"event.name",
"event.text",
"event.timestamp",
"event.version"
});
}
[Fact]
public void Should_describe_schema_trigger()
{
var result = sut.SchemaTrigger();
AssertSchemaTrigger(result);
}
[Fact]
public void Should_describe_dynamic_schema_trigger()
{
var result = sut.Trigger("SchemaChanged");
AssertSchemaTrigger(result);
}
private static void AssertSchemaTrigger(IReadOnlyList<ScriptingValue> result)
{
AssertCompletion(result,
PresetActor("event.actor"),
PresetUser("event.user"),
new[]
{
"event",
"event.appId",
"event.appId.id",
"event.appId.name",
"event.name",
"event.schemaId",
"event.schemaId.id",
"event.schemaId.name",
"event.timestamp",
"event.type",
"event.version"
});
}
[Fact]
public void Should_describe_usage_trigger()
{
var result = sut.UsageTrigger();
AssertUsageTrigger(result);
}
[Fact]
public void Should_describe_dynamic_usage_trigger()
{
var result = sut.Trigger("Usage");
AssertUsageTrigger(result);
}
private static void AssertUsageTrigger(IReadOnlyList<ScriptingValue> result)
{
AssertCompletion(result,
new[]
{
"event",
"event.appId",
"event.appId.id",
"event.appId.name",
"event.callsCurrent",
"event.callsLimit",
"event.name",
"event.timestamp",
"event.version"
});
}
private static void AssertCompletion(IReadOnlyList<ScriptingValue> result, params string[][] expected)
{
var allExpected = expected.SelectMany(x => x).ToArray();
var paths = result.Select(x => x.Path).ToArray();
foreach (var value in paths)
{
Assert.Contains(value, allExpected);
}
foreach (var value in allExpected)
{
Assert.Contains(value, paths);
}
}
private static string[] PresetActor(string path)
{
return new[]
{
$"{path}",
$"{path}.identifier",
$"{path}.type"
};
}
private static string[] PresetUser(string path)
{
return new[]
{
$"{path}",
$"{path}.claims",
$"{path}.claims.name",
$"{path}.email",
$"{path}.id",
$"{path}.isClient",
$"{path}.isUser",
};
}
}
}

82
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs

@ -99,8 +99,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId, false, default))
.Returns((app, schema));
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, A<string>._, ScriptOptions(), default))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<ScriptVars>(0)!.Data!));
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), default))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<DataScriptVars>(0)!.Data!));
patched = patch.MergeInto(data);
@ -150,9 +150,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -173,9 +173,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -197,9 +197,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -250,9 +250,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -273,9 +273,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -297,9 +297,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft), "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -321,7 +321,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentUpdated { Data = otherData })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -343,7 +343,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentUpdated { Data = otherData })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -367,9 +367,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(otherData, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -423,7 +423,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentUpdated { Data = otherData })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -447,7 +447,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentUpdated { Data = otherData })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(otherData, data, Status.Draft), "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -464,7 +464,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<update-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -496,7 +496,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentUpdated { Data = patched })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -520,7 +520,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentUpdated { Data = patched })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -537,7 +537,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<update-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -559,7 +559,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -581,7 +581,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -604,7 +604,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -613,7 +613,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new ChangeContentStatus { Status = Status.Draft };
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default))
.Returns(otherData);
await ExecuteCreateAsync();
@ -632,7 +632,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Draft, Status.Published), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -656,7 +656,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Archived, Status.Draft), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -680,7 +680,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Published, Status = Status.Published })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptVars(data, null, Status.Published, Status.Draft), "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(data, null, Status.Published, Status.Draft), "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -707,7 +707,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -735,7 +735,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
);
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -763,7 +763,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentSchedulingCancelled())
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(A<DataScriptVars>._, "<change-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
@ -845,7 +845,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
CreateContentEvent(new ContentDeleted())
);
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(DataScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -863,7 +863,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version);
Assert.Empty(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default))
A.CallTo(() => scriptEngine.ExecuteAsync(DataScriptVars(data, null, Status.Draft), "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
}
@ -969,17 +969,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
return A<ScriptOptions>.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext);
}
private ScriptVars ScriptVars(ContentData? newData, ContentData? oldData, Status newStatus)
private DataScriptVars DataScriptVars(ContentData? newData, ContentData? oldData, Status newStatus)
{
return A<ScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, default));
return A<DataScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, default));
}
private ScriptVars ScriptVars(ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus)
private DataScriptVars DataScriptVars(ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus)
{
return A<ScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, oldStatus));
return A<DataScriptVars>.That.Matches(x => Matches(x, newData, oldData, newStatus, oldStatus));
}
private bool Matches(ScriptVars x, ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus)
private bool Matches(DataScriptVars x, ContentData? newData, ContentData? oldData, Status newStatus, Status oldStatus)
{
return
Equals(x["contentId"], contentId) &&

1
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -19,7 +19,6 @@ using Squidex.Caching;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Domain.Apps.Entities.Contents.TestData;
using Squidex.Domain.Apps.Entities.Schemas;

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs

@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(ctx, new[] { content }, provider, default);
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._))
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._))
.MustNotHaveHappened();
}
@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(ctx, new[] { content }, provider, default);
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._))
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, A<string>._, ScriptOptions(), A<CancellationToken>._))
.MustNotHaveHappened();
}
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var content = new ContentEntity { Data = oldData, SchemaId = schemaId };
A.CallTo(() => scriptEngine.TransformAsync(A<ScriptVars>._, "my-query", ScriptOptions(), A<CancellationToken>._))
A.CallTo(() => scriptEngine.TransformAsync(A<DataScriptVars>._, "my-query", ScriptOptions(), A<CancellationToken>._))
.Returns(new ContentData());
await sut.EnrichAsync(ctx, new[] { content }, provider, default);
@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.NotSame(oldData, content.Data);
A.CallTo(() => scriptEngine.TransformAsync(
A<ScriptVars>.That.Matches(x =>
A<DataScriptVars>.That.Matches(x =>
Equals(x["user"], ctx.User) &&
Equals(x["data"], oldData) &&
Equals(x["contentId"], content.Id)),

7
frontend/src/app/features/rules/pages/rule/rule-page.component.html

@ -133,7 +133,12 @@
{{ 'rules.actionHint' | sqxTranslate }}
</sqx-form-alert>
<sqx-generic-action [actionForm]="currentAction.form"></sqx-generic-action>
<sqx-generic-action
[actionForm]="currentAction.form"
[trigger]="currentTrigger"
[triggerType]="currentTrigger?.type"
[appName]="rulesState.appName">
</sqx-generic-action>
</ng-container>
<ng-template #actionSelector>

2
frontend/src/app/features/rules/shared/actions/formattable-input.component.html

@ -17,5 +17,5 @@
</button>
</div>
<sqx-code-editor [height]="350" [mode]="aceMode"></sqx-code-editor>
<sqx-code-editor [completion]="actualCompletion" [height]="350" [mode]="aceMode"></sqx-code-editor>
</ng-template>

2
frontend/src/app/features/rules/shared/actions/formattable-input.component.scss

@ -25,7 +25,7 @@
color: $color-text-decent;
&.active {
color: $color-text;
color: $color-black;
}
}
}

7
frontend/src/app/features/rules/shared/actions/formattable-input.component.ts

@ -37,6 +37,9 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie
@Input()
public formattable = true;
@Input()
public completion: ReadonlyArray<{ path: string; description: string }> | undefined | null;
@ViewChild(DefaultValueAccessor)
public inputEditor!: DefaultValueAccessor;
@ -50,6 +53,10 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie
public aceMode = 'ace/editor/text';
public get actualCompletion() {
return this.mode === 'Script' ? this.completion : null;
}
public get valueAccessor(): ControlValueAccessor {
return this.codeEditor || this.inputEditor;
}

8
frontend/src/app/features/rules/shared/actions/generic-action.component.html

@ -12,10 +12,10 @@
<sqx-formattable-input [formControlName]="property.name" type="Text" [formattable]="property.isFormattable"></sqx-formattable-input>
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<sqx-formattable-input [formControlName]="property.name" type="Code" [formattable]="property.isFormattable"></sqx-formattable-input>
<sqx-formattable-input [completion]="ruleCompletions | async" [formControlName]="property.name" type="Code" [formattable]="property.isFormattable"></sqx-formattable-input>
</ng-container>
<ng-container *ngSwitchCase="'Javascript'">
<sqx-code-editor [formControlName]="property.name" [height]="350"></sqx-code-editor>
<sqx-code-editor [completion]="ruleCompletions | async" [formControlName]="property.name" [height]="350"></sqx-code-editor>
</ng-container>
<ng-container *ngSwitchCase="'Checkbox'">
<div class="form-check">
@ -37,10 +37,10 @@
</ng-container>
<sqx-form-hint>
<span [sqxMarkdown]="property.description" [inline]="true"></span>
<span [sqxMarkdown]="property.description" [inline]="true" [html]="true"></span>
<div *ngIf="property.isFormattable">
You can use advanced formatting: <a href="https://docs.squidex.io/concepts/rules#3-formatting" sqxExternalLink>Documentation</a>
{{ 'i18n:rules.advancedFormattingHint' | sqxTranslate }}: <a href="https://docs.squidex.io/concepts/rules#3-formatting" sqxExternalLink>{{ 'i18n:common.documentation' | sqxTranslate }}</a>
</div>
</sqx-form-hint>
</div>

35
frontend/src/app/features/rules/shared/actions/generic-action.component.ts

@ -5,16 +5,43 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ActionForm } from '@app/shared';
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { EMPTY, Observable, shareReplay } from 'rxjs';
import { ActionForm, RuleCompletions, RulesService } from '@app/shared';
@Component({
selector: 'sqx-generic-action[actionForm]',
selector: 'sqx-generic-action[actionForm][appName][trigger][triggerType]',
styleUrls: ['./generic-action.component.scss'],
templateUrl: './generic-action.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GenericActionComponent {
export class GenericActionComponent implements OnChanges {
@Input()
public actionForm!: ActionForm;
@Input()
public appName!: string;
@Input()
public trigger!: any;
@Input()
public triggerType: string | undefined | null;
public ruleCompletions: Observable<RuleCompletions> = EMPTY;
constructor(
private readonly rulesService: RulesService,
) {
}
public ngOnChanges(changes: SimpleChanges) {
if (changes['appName'] || changes['triggerType']) {
if (this.triggerType) {
this.ruleCompletions = this.rulesService.getCompletions(this.appName, this.triggerType).pipe(shareReplay(1));
} else {
this.ruleCompletions = EMPTY;
}
}
}
}

2
frontend/src/app/features/settings/pages/asset-scripts/asset-scripts-page.component.ts

@ -15,7 +15,7 @@ import { AppsState, AssetCompletions, AssetScriptsState, AssetsService, EditAsse
templateUrl: './asset-scripts-page.component.html',
})
export class AssetScriptsPageComponent extends ResourceOwner implements OnInit {
public assetScript = 'create';
public assetScript = 'Annotate';
public assetCompletions: Observable<AssetCompletions> = EMPTY;
public editForm = new EditAssetScriptsForm();

2
frontend/src/app/features/settings/pages/backups/backups-page.component.ts

@ -24,7 +24,7 @@ export class BackupsPageComponent extends ResourceOwner implements OnInit {
}
public ngOnInit() {
this.backupsState.load();
this.backupsState.load(true);
this.own(timer(3000, 3000).pipe(switchMap(() => this.backupsState.load(false, true))));
}

5
frontend/src/app/framework/angular/markdown.directive.ts

@ -35,6 +35,9 @@ export class MarkdownDirective implements OnChanges {
@Input()
public inline = true;
@Input()
public html = false;
@Input()
public optional = false;
@ -59,7 +62,7 @@ export class MarkdownDirective implements OnChanges {
html = marked(this.markdown, { renderer });
}
if (!html || html === this.markdown || html.indexOf('<') < 0) {
if (!this.html && (!html || html === this.markdown || html.indexOf('<') < 0)) {
this.renderer.setProperty(this.element.nativeElement, 'textContent', html);
} else {
this.renderer.setProperty(this.element.nativeElement, 'innerHTML', html);

20
frontend/src/app/shared/services/assets.service.spec.ts

@ -7,7 +7,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { AnalyticsService, ApiUrlConfig, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsDto, AssetsService, DateTime, ErrorDto, MathHelper, Resource, ResourceLinks, sanitize, Version } from '@app/shared/internal';
import { AnalyticsService, ApiUrlConfig, AssetCompletions, AssetDto, AssetFolderDto, AssetFoldersDto, AssetsDto, AssetsService, DateTime, ErrorDto, MathHelper, Resource, ResourceLinks, sanitize, Version } from '@app/shared/internal';
describe('AssetsService', () => {
const version = new Version('1');
@ -453,6 +453,24 @@ describe('AssetsService', () => {
expect(assetFolder!).toEqual(createAssetFolder(22));
}));
it('should make get request to get completions',
inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => {
let completions: AssetCompletions;
assetsService.getCompletions('my-app').subscribe(result => {
completions = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/completion');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([]);
expect(completions!).toEqual([]);
}));
function assetResponse(id: number, suffix = '', parentId?: string) {
parentId = parentId || MathHelper.EMPTY_GUID;

19
frontend/src/app/shared/services/rules.service.spec.ts

@ -8,6 +8,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { AnalyticsService, ApiUrlConfig, DateTime, Resource, ResourceLinks, RuleDto, RuleElementDto, RuleElementPropertyDto, RuleEventDto, RuleEventsDto, RulesDto, RulesService, Version } from '@app/shared/internal';
import { RuleCompletions } from '..';
import { SimulatedRuleEventDto, SimulatedRuleEventsDto } from './rules.service';
describe('RulesService', () => {
@ -361,6 +362,24 @@ describe('RulesService', () => {
req.flush({});
}));
it('should make get request to get completions',
inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => {
let completions: RuleCompletions;
rulesService.getCompletions('my-app', 'TriggerType').subscribe(result => {
completions = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/rules/completion/TriggerType');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([]);
expect(completions!).toEqual([]);
}));
function ruleResponse(id: number, suffix = '') {
const key = `${id}${suffix}`;

9
frontend/src/app/shared/services/rules.service.ts

@ -210,6 +210,9 @@ export class SimulatedRuleEventDto {
}
}
export type RuleCompletions =
ReadonlyArray<{ path: string; description: string; type: string }>;
export type ActionsDto =
Readonly<{ [name: string]: RuleElementDto }>;
@ -380,6 +383,12 @@ export class RulesService {
}),
pretifyError('i18n:rules.ruleEvents.cancelFailed'));
}
public getCompletions(appName: string, actionType: string): Observable<RuleCompletions> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/rules/completion/${actionType}`);
return this.http.get<RuleCompletions>(url);
}
}
function parseSimulatedEvents(response: { items: any[]; total: number } & Resource) {

19
frontend/src/app/shared/services/schemas.service.spec.ts

@ -8,6 +8,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { AnalyticsService, ApiUrlConfig, createProperties, DateTime, FieldRule, NestedFieldDto, Resource, ResourceLinks, RootFieldDto, SchemaDto, SchemaPropertiesDto, SchemasDto, SchemasService, Version } from '@app/shared/internal';
import { SchemaCompletions } from '..';
describe('SchemasService', () => {
const version = new Version('1');
@ -578,6 +579,24 @@ describe('SchemasService', () => {
req.flush({});
}));
it('should make get request to get completions',
inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => {
let completions: SchemaCompletions;
schemasService.getCompletions('my-app', 'my-schema').subscribe(result => {
completions = result;
});
const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/completion');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([]);
expect(completions!).toEqual([]);
}));
function schemaPropertiesResponse(id: number, suffix = '') {
const key = `${id}${suffix}`;

8
frontend/src/app/shared/state/apps.state.ts

@ -33,14 +33,14 @@ export class AppsState extends State<Snapshot> {
public selectedSettings =
this.project(s => s.selectedSettings);
public get appName() {
return this.snapshot.selectedApp?.name || '';
}
public get appId() {
return this.snapshot.selectedApp?.id || '';
}
public get appName() {
return this.snapshot.selectedApp?.name || '';
}
constructor(
private readonly appsService: AppsService,
private readonly dialogs: DialogService,

12
frontend/src/app/shared/state/asset-scripts.state.ts

@ -46,6 +46,14 @@ export class AssetScriptsState extends State<Snapshot> {
public canUpdate =
this.project(x => x.canUpdate === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly appsService: AppsService,
@ -100,10 +108,6 @@ export class AssetScriptsState extends State<Snapshot> {
}, 'Loading Success / Updated');
}
private get appName() {
return this.appsState.appName;
}
private get version() {
return this.snapshot.version;
}

12
frontend/src/app/shared/state/asset-uploader.state.ts

@ -44,6 +44,14 @@ export class AssetUploaderState extends State<Snapshot> {
public uploads =
this.project(x => x.uploads);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
@ -148,8 +156,4 @@ export class AssetUploaderState extends State<Snapshot> {
return { ...s, uploads };
}, 'Upload Started');
}
private get appName() {
return this.appsState.appName;
}
}

20
frontend/src/app/shared/state/assets.state.ts

@ -113,6 +113,18 @@ export abstract class AssetsStateBase extends State<Snapshot> {
public canRenameTag =
this.project(x => x.canRenameTag === true);
public get parentId() {
return this.snapshot.parentId;
}
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
protected constructor(name: string,
private readonly appsState: AppsState,
private readonly assetsService: AssetsService,
@ -414,14 +426,6 @@ export abstract class AssetsStateBase extends State<Snapshot> {
return this.loadInternal(false);
}
public get parentId() {
return this.snapshot.parentId;
}
private get appName() {
return this.appsState.appName;
}
}
function getTagNames(tags: object): ReadonlyArray<string> {

12
frontend/src/app/shared/state/backups.state.ts

@ -45,6 +45,14 @@ export class BackupsState extends State<Snapshot> {
public canCreate =
this.project(x => x.canCreate === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly backupsService: BackupsService,
@ -100,8 +108,4 @@ export class BackupsState extends State<Snapshot> {
}),
shareSubscribed(this.dialogs));
}
private get appName() {
return this.appsState.appName;
}
}

12
frontend/src/app/shared/state/clients.state.ts

@ -45,6 +45,14 @@ export class ClientsState extends State<Snapshot> {
public canCreate =
this.project(x => x.canCreate === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly clientsService: ClientsService,
@ -114,10 +122,6 @@ export class ClientsState extends State<Snapshot> {
}, 'Loading Success / Updated');
}
private get appName() {
return this.appsState.appName;
}
private get version() {
return this.snapshot.version;
}

8
frontend/src/app/shared/state/contents.state.ts

@ -81,14 +81,14 @@ export abstract class ContentsStateBase extends State<Snapshot> {
public statusQueries =
this.projectFrom(this.statuses, getStatusQueries);
public get appName() {
return this.appsState.appName;
}
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
protected constructor(name: string,
private readonly appsState: AppsState,
private readonly contentsService: ContentsService,

8
frontend/src/app/shared/state/contributors.state.ts

@ -59,6 +59,10 @@ export class ContributorsState extends State<Snapshot> {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly contributorsService: ContributorsService,
@ -150,10 +154,6 @@ export class ContributorsState extends State<Snapshot> {
}, 'Loading Success / Updated');
}
private get appName() {
return this.appsState.appName;
}
private get version() {
return this.snapshot.version;
}

12
frontend/src/app/shared/state/languages.state.ts

@ -76,6 +76,14 @@ export class LanguagesState extends State<Snapshot> {
public canCreate =
this.project(x => x.canCreate === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appLanguagesService: AppLanguagesService,
private readonly appsState: AppsState,
@ -165,10 +173,6 @@ export class LanguagesState extends State<Snapshot> {
}, 'Loading Success / Updated');
}
private get appName() {
return this.appsState.appName;
}
private get version() {
return this.snapshot.version;
}

12
frontend/src/app/shared/state/plans.state.ts

@ -64,12 +64,16 @@ export class PlansState extends State<Snapshot> {
public hasPortal =
this.project(x => x.hasPortal);
public window = window;
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
public window = window;
constructor(
private readonly appsState: AppsState,
private readonly authState: AuthService,
@ -130,10 +134,6 @@ export class PlansState extends State<Snapshot> {
shareSubscribed(this.dialogs));
}
private get appName() {
return this.appsState.appName;
}
private get userId() {
return this.authState.user!.id;
}

12
frontend/src/app/shared/state/roles.state.ts

@ -51,6 +51,14 @@ export class RolesState extends State<Snapshot> {
public canCreate =
this.project(x => x.canCreate === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly dialogs: DialogService,
@ -120,10 +128,6 @@ export class RolesState extends State<Snapshot> {
}, 'Loading Success / Updated');
}
private get appName() {
return this.appsState.appName;
}
private get version() {
return this.snapshot.version;
}

12
frontend/src/app/shared/state/rule-events.state.ts

@ -43,6 +43,14 @@ export class RuleEventsState extends State<Snapshot> {
public canCancelAll =
this.project(x => hasAnyLink(x.links, 'cancel'));
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly dialogs: DialogService,
@ -140,10 +148,6 @@ export class RuleEventsState extends State<Snapshot> {
return this.loadInternal(false);
}
private get appName() {
return this.appsState.appName;
}
}
const setCancelled = (event: RuleEventDto) =>

12
frontend/src/app/shared/state/rule-simulator.state.ts

@ -34,6 +34,14 @@ export class RuleSimulatorState extends State<Snapshot> {
public isLoading =
this.project(x => x.isLoading === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly dialogs: DialogService,
@ -86,8 +94,4 @@ export class RuleSimulatorState extends State<Snapshot> {
public selectRule(ruleId?: string) {
this.resetState({ ruleId }, 'Select Rule');
}
private get appName() {
return this.appsState.appName;
}
}

12
frontend/src/app/shared/state/rules.state.ts

@ -69,6 +69,14 @@ export class RulesState extends State<Snapshot> {
public runningRule =
this.projectFrom2(this.rules, this.runningRuleId, (r, id) => r.find(x => x.id === id));
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly dialogs: DialogService,
@ -216,8 +224,4 @@ export class RulesState extends State<Snapshot> {
return { ...s, rules, selectedRule };
}, 'Updated');
}
private get appName() {
return this.appsState.appName;
}
}

12
frontend/src/app/shared/state/schemas.state.ts

@ -62,6 +62,14 @@ export class SchemasState extends State<Snapshot> {
public categoryNames =
this.projectFrom2(this.schemas, this.addedCategories, (s, c) => buildCategoryNames(c, s));
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
public get schemaId() {
return this.snapshot.selectedSchema?.id || '';
}
@ -352,10 +360,6 @@ export class SchemasState extends State<Snapshot> {
this.dialogs.notifyInfo('i18n:common.nothingChanged');
}
}
private get appName() {
return this.appsState.appName;
}
}
function getField(x: SchemaDto, request: AddFieldDto, parent?: RootFieldDto | null): FieldDto {

12
frontend/src/app/shared/state/ui.state.ts

@ -84,6 +84,14 @@ export class UIState extends State<Snapshot> {
distinctUntilChanged());
}
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly uiService: UIService,
@ -208,10 +216,6 @@ export class UIState extends State<Snapshot> {
return false;
}
private get appName() {
return this.appsState.appName;
}
}
function getValue<T>(setting: Settings | undefined | null, path: string, defaultValue: T) {

12
frontend/src/app/shared/state/workflows.state.ts

@ -49,6 +49,14 @@ export class WorkflowsState extends State<Snapshot> {
public canCreate =
this.project(x => x.canCreate === true);
public get appId() {
return this.appsState.appId;
}
public get appName() {
return this.appsState.appName;
}
constructor(
private readonly appsState: AppsState,
private readonly dialogs: DialogService,
@ -121,10 +129,6 @@ export class WorkflowsState extends State<Snapshot> {
}, 'Loading Success / Updated');
}
private get appName() {
return this.appsState.appName;
}
private get version() {
return this.snapshot.version;
}

Loading…
Cancel
Save