Browse Source

Feature/asset scripts (#759)

* Initial commit for new feature.

* Fixes.

* Final fixes.

* Fix merge issue.

* A few last fixes.

* Compiler fix.
pull/768/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
da534ce3ec
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs
  2. 1
      backend/i18n/frontend_en.json
  3. 1
      backend/i18n/frontend_it.json
  4. 1
      backend/i18n/frontend_nl.json
  5. 1
      backend/i18n/frontend_zh.json
  6. 1
      backend/i18n/source/frontend_en.json
  7. 24
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetScripts.cs
  8. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
  9. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs
  10. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs
  11. 237
      backend/src/Squidex.Domain.Apps.Core.Operations/FieldDescriptions.cs
  12. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs
  13. 51
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
  14. 85
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  15. 104
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompletion.cs
  16. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
  18. 16
      backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs
  19. 13
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs
  20. 15
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs
  21. 7
      backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs
  22. 3
      backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
  23. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  24. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs
  25. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  26. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs
  28. 123
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs
  29. 57
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs
  30. 45
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderOperation.cs
  31. 45
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetOperation.cs
  32. 55
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs
  33. 82
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAssetFolder.cs
  34. 128
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs
  35. 265
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs
  36. 32
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs
  37. 93
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ValidationExtensions.cs
  38. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  39. 92
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  40. 62
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperation.cs
  41. 169
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs
  42. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs
  43. 16
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs
  44. 68
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs
  45. 46
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs
  46. 127
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/OperationContext.cs
  47. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs
  48. 53
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs
  49. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs
  50. 59
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  51. 29
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  52. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs
  53. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResultGraphType.cs
  54. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/UserGraphType.cs
  55. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  56. 20
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  57. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  58. 28
      backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs
  59. 85
      backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs
  60. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  61. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs
  62. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs
  63. 2
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs
  64. 20
      backend/src/Squidex.Domain.Apps.Entities/Scripting/JsonType.cs
  65. 316
      backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingCompletion.cs
  66. 15
      backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingValue.cs
  67. 18
      backend/src/Squidex.Domain.Apps.Events/Apps/AppAssetsScriptsConfigured.cs
  68. 2
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs
  69. 2
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs
  70. 2
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs
  71. 2
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs
  72. 4
      backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUIFieldsConfigured.cs
  73. 12
      backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs
  74. 6
      backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs
  75. 19
      backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs
  76. 2
      backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs
  77. 4
      backend/src/Squidex.Shared/Permissions.cs
  78. 4
      backend/src/Squidex.Web/Resources.cs
  79. 15
      backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs
  80. 90
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs
  81. 90
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs
  82. 45
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  83. 7
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  84. 4
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs
  85. 67
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AssetScriptsDto.cs
  86. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppSettingsDto.cs
  87. 48
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAssetScripts.cs
  88. 15
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  89. 5
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs
  90. 13
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs
  91. 3
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs
  92. 9
      backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  93. 49
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  94. 14
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs
  95. 16
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  96. 88
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs
  97. 23
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs
  98. 144
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs
  99. 116
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs
  100. 115
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs

2
backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs

@ -61,7 +61,7 @@ namespace Squidex.Extensions.Actions.SignalR
HubName = hubName, HubName = hubName,
MethodName = action.MethodName, MethodName = action.MethodName,
MethodPayload = requestBody, MethodPayload = requestBody,
Targets = target.Split("\n"), Targets = target.Split("\n")
}; };
return (ruleDescription, ruleJob); return (ruleDescription, ruleJob);

1
backend/i18n/frontend_en.json

@ -214,6 +214,7 @@
"common.apps": "Apps", "common.apps": "Apps",
"common.aspectRatio": "AspectRatio", "common.aspectRatio": "AspectRatio",
"common.assets": "Assets", "common.assets": "Assets",
"common.assetScripts": "Asset Scripts",
"common.back": "Back", "common.back": "Back",
"common.backendError": "Backend ERROR", "common.backendError": "Backend ERROR",
"common.backups": "Backups", "common.backups": "Backups",

1
backend/i18n/frontend_it.json

@ -214,6 +214,7 @@
"common.apps": "App", "common.apps": "App",
"common.aspectRatio": "Proporzioni", "common.aspectRatio": "Proporzioni",
"common.assets": "Risorse", "common.assets": "Risorse",
"common.assetScripts": "Asset Scripts",
"common.back": "Indietro", "common.back": "Indietro",
"common.backendError": "Errore nel Backend", "common.backendError": "Errore nel Backend",
"common.backups": "Backup", "common.backups": "Backup",

1
backend/i18n/frontend_nl.json

@ -214,6 +214,7 @@
"common.apps": "Apps", "common.apps": "Apps",
"common.aspectRatio": "AspectRatio", "common.aspectRatio": "AspectRatio",
"common.assets": "Bestanden", "common.assets": "Bestanden",
"common.assetScripts": "Asset Scripts",
"common.back": "Terug", "common.back": "Terug",
"common.backendError": "Backend ERROR", "common.backendError": "Backend ERROR",
"common.backups": "Back-ups", "common.backups": "Back-ups",

1
backend/i18n/frontend_zh.json

@ -214,6 +214,7 @@
"common.apps": "应用程序", "common.apps": "应用程序",
"common.aspectRatio": "纵横比", "common.aspectRatio": "纵横比",
"common.assets": "资源", "common.assets": "资源",
"common.assetScripts": "Asset Scripts",
"common.back": "返回", "common.back": "返回",
"common.backendError": "后端错误", "common.backendError": "后端错误",
"common.backups": "备份", "common.backups": "备份",

1
backend/i18n/source/frontend_en.json

@ -214,6 +214,7 @@
"common.apps": "Apps", "common.apps": "Apps",
"common.aspectRatio": "AspectRatio", "common.aspectRatio": "AspectRatio",
"common.assets": "Assets", "common.assets": "Assets",
"common.assetScripts": "Asset Scripts",
"common.back": "Back", "common.back": "Back",
"common.backendError": "Backend ERROR", "common.backendError": "Backend ERROR",
"common.backups": "Backups", "common.backups": "Backups",

24
backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetScripts.cs

@ -0,0 +1,24 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Assets
{
public sealed record AssetScripts
{
public static readonly AssetScripts Empty = new AssetScripts();
public string? Create { get; init; }
public string? Update { get; init; }
public string? Annotate { get; init; }
public string? Move { get; init; }
public string? Delete { get; init; }
}
}

16
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string Name { get; } public string Name { get; }
public string Category { get; private set; } public string? Category { get; private set; }
public bool IsPublished { get; private set; } public bool IsPublished { get; private set; }
@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Schema Update(SchemaProperties newProperties) public Schema Update(SchemaProperties? newProperties)
{ {
newProperties ??= new SchemaProperties(); newProperties ??= new SchemaProperties();
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Schema SetScripts(SchemaScripts newScripts) public Schema SetScripts(SchemaScripts? newScripts)
{ {
newScripts ??= new SchemaScripts(); newScripts ??= new SchemaScripts();
@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Schema SetFieldsInLists(FieldNames names) public Schema SetFieldsInLists(FieldNames? names)
{ {
names ??= FieldNames.Empty; names ??= FieldNames.Empty;
@ -132,7 +132,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Schema SetFieldsInReferences(FieldNames names) public Schema SetFieldsInReferences(FieldNames? names)
{ {
names ??= FieldNames.Empty; names ??= FieldNames.Empty;
@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Schema SetFieldRules(FieldRules rules) public Schema SetFieldRules(FieldRules? rules)
{ {
rules ??= FieldRules.Empty; rules ??= FieldRules.Empty;
@ -204,7 +204,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Schema ChangeCategory(string category) public Schema ChangeCategory(string? category)
{ {
if (string.Equals(Category, category)) if (string.Equals(Category, category))
{ {
@ -218,7 +218,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
} }
[Pure] [Pure]
public Schema SetPreviewUrls(ImmutableDictionary<string, string> previewUrls) public Schema SetPreviewUrls(ImmutableDictionary<string, string>? previewUrls)
{ {
previewUrls ??= ImmutableDictionary.Empty<string, string>(); previewUrls ??= ImmutableDictionary.Empty<string, string>();

10
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaScripts.cs

@ -11,14 +11,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
public static readonly SchemaScripts Empty = new SchemaScripts(); public static readonly SchemaScripts Empty = new SchemaScripts();
public string Change { get; init; } public string? Change { get; init; }
public string Create { get; init; } public string? Create { get; init; }
public string Update { get; init; } public string? Update { get; init; }
public string Delete { get; init; } public string? Delete { get; init; }
public string Query { get; init; } public string? Query { get; init; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SyncHelpers.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
return lhs == (rhs ?? false); return lhs == (rhs ?? false);
} }
public static bool StringEquals(this string lhs, string rhs) public static bool StringEquals(this string? lhs, string? rhs)
{ {
return string.Equals(lhs ?? string.Empty, rhs ?? string.Empty, StringComparison.Ordinal); return string.Equals(lhs ?? string.Empty, rhs ?? string.Empty, StringComparison.Ordinal);
} }

237
backend/src/Squidex.Domain.Apps.Core.Operations/FieldDescriptions.cs

@ -0,0 +1,237 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core
{
public static class FieldDescriptions
{
public static string AppId =>
"The ID of the current app.";
public static string AppName =>
"The name of the current app.";
public static string Asset =>
"The asset.";
public static string AssetFileHash =>
"The hash of the file. Can be null for old files.";
public static string AssetFileName =>
"The file name of the asset.";
public static string AssetFileSize =>
"The size of the file in bytes.";
public static string AssetFileType =>
"The file type (file extension) of the asset.";
public static string AssetFileVersion =>
"The version of the file.";
public static string AssetIsImage =>
"Determines if the uploaded file is an image.";
public static string AssetIsProtected =>
"True, when the asset is not public.";
public static string AssetMetadata =>
"The asset metadata.";
public static string AssetMetadataText =>
"The type of the image.";
public static string AssetMetadataValue =>
"The asset metadata with name 'name'.";
public static string AssetMimeType =>
"The mime type.";
public static string AssetParentId =>
"The id of the parent folder. Empty for files without parent.";
public static string AssetParentPath =>
"The full path in the folder hierarchy as array of folder infos.";
public static string AssetPixelHeight =>
"The height of the image in pixels if the asset is an image.";
public static string AssetPixelWidth =>
"The width of the image in pixels if the asset is an image.";
public static string AssetsItems =>
"The assets.";
public static string AssetSlug =>
"The file name as slug.";
public static string AssetSourceUrl =>
"The source URL of the asset.";
public static string AssetsTotal =>
"The total count of assets.";
public static string AssetTags =>
"The asset tags.";
public static string AssetThumbnailUrl =>
"The thumbnail URL to the asset.";
public static string AssetType =>
"The type of the image.";
public static string AssetUrl =>
"The URL to the asset.";
public static string Command =>
"The executed command.";
public static string ContentData =>
"The data of the content.";
public static string ContentDataOld =>
"The previous data of the content.";
public static string ContentFlatData =>
"The flat data of the content.";
public static string ContentNewStatus =>
"The new status of the content.";
public static string ContentNewStatusColor =>
"The new status color of the content.";
public static string ContentRequestData =>
"The data for the content.";
public static string ContentRequestDueTime =>
"The timestamp when the status should be changed.";
public static string ContentRequestOptionalId =>
"The optional custom content ID.";
public static string ContentRequestOptionalStatus =>
"The initial status.";
public static string ContentRequestPublish =>
"Set to true to autopublish content on create.";
public static string ContentRequestStatus =>
"The status for the content.";
public static string ContentSchema =>
"The name of the schema.";
public static string ContentSchemaId =>
"The ID of the schema.";
public static string ContentSchemaName =>
"The display name of the schema.";
public static string ContentsItems =>
$"The contents.";
public static string ContentStatus =>
"The status of the content.";
public static string ContentStatusColor =>
"The status color of the content.";
public static string ContentStatusOld =>
"The previous status of the content.";
public static string ContentsTotal =>
$"The total count of contents.";
public static string ContentUrl =>
"The URL to the content.";
public static string Context =>
"The context object holding all values.";
public static string EntityCreated =>
"The timestamp when the object was created.";
public static string EntityCreatedBy =>
"The user who created the object.";
public static string EntityExpectedVersion =>
"The expected version.";
public static string EntityId =>
"The ID of the object.";
public static string EntityIsDeleted =>
"True when deleted.";
public static string EntityLastModified =>
"The timestamp when the object was updated the last time.";
public static string EntityLastModifiedBy =>
"The user who updated the object the last time.";
public static string EntityRequestDeletePermanent =>
"True when the entity should be deleted permanently.";
public static string EntityVersion =>
"The version of the object (usually GUID).";
public static string JsonPath =>
"The path to the json value.";
public static string Operation =>
"The current operation.";
public static string QueryFilter =>
"Optional OData filter.";
public static string QueryIds =>
"Comma separated list of object IDs. Overrides all other query parameters.";
public static string QueryOrderBy =>
"Optional OData order definition.";
public static string QueryQ =>
"JSON query as well formatted json string. Overrides all other query parameters, except 'ids'.";
public static string QuerySearch =>
"Optional OData full text search.";
public static string QuerySkip =>
"Optional number of contents to skip.";
public static string QueryTop =>
"Optional number of contents to take.";
public static string QueryVersion =>
"The optional version of the content to retrieve an older instance (not cached).";
public static string User =>
"Information about the current user.";
public static string UserClaims =>
"The additional properties of the user.";
public static string UserDisplayName =>
"The display name of the user.";
public static string UserEmail =>
"The email address of the current user.";
public static string UserId =>
"The ID of the user.";
public static string UserIsClient =>
"True when the current user is a client, which is typically the case when the request is made from the API.";
public static string UserIsUser =>
"True when the current user is a user, which is typically the case when the request is made in the UI.";
public static string UsersClaimsValue =>
"The list of additional properties that have the name 'name'.";
}
}

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/SkipReason.cs

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
[Flags] [Flags]
public enum SkipReason public enum SkipReason
{ {
None, None = 0,
ConditionDoesNotMatch = 1 << 0, ConditionDoesNotMatch = 1 << 0,
ConditionPrecheckDoesNotMatch = 1 << 1, ConditionPrecheckDoesNotMatch = 1 << 1,
Disabled = 1 << 2, Disabled = 1 << 2,

51
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs

@ -9,7 +9,9 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using Jint; using Jint;
using Jint.Native;
using Jint.Runtime.Interop; using Jint.Runtime.Interop;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users; using Squidex.Shared.Users;
@ -20,32 +22,33 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
private static readonly char[] ClaimSeparators = { '/', '.', ':' }; private static readonly char[] ClaimSeparators = { '/', '.', ':' };
public static ObjectWrapper Create(Engine engine, IUser user) public static JsValue Create(Engine engine, IUser user)
{ {
var clientId = user.Claims.FirstOrDefault(x => x.Type == OpenIdClaims.ClientId)?.Value; var isClient = user.Claims.Any(x => x.Type == OpenIdClaims.ClientId);
var isClient = !string.IsNullOrWhiteSpace(clientId); return CreateUser(
engine,
return CreateUser(engine, user.Id, isClient, user.Email, user.Claims.DisplayName(), user.Claims); user.Id,
isClient,
user.Email,
user.Claims.DisplayName(),
user.Claims);
} }
public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal) public static JsValue Create(Engine engine, ClaimsPrincipal principal)
{ {
var id = principal.OpenIdSubject()!; var token = principal.Token();
var isClient = string.IsNullOrWhiteSpace(id);
if (isClient)
{
id = principal.OpenIdClientId()!;
}
var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value; return CreateUser(
engine,
return CreateUser(engine, id, isClient, principal.OpenIdEmail()!, name, principal.Claims); token?.Identifier ?? string.Empty,
token?.Type != RefTokenType.Subject,
principal.OpenIdEmail()!,
principal.Claims.DisplayName(),
principal.Claims);
} }
private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable<Claim> allClaims) private static JsValue CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable<Claim> allClaims)
{ {
var claims = var claims =
allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last()) allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last())
@ -53,7 +56,17 @@ namespace Squidex.Domain.Apps.Core.Scripting
x => x.Key, x => x.Key,
x => x.Select(y => y.Value).ToArray()); x => x.Select(y => y.Value).ToArray());
return new ObjectWrapper(engine, new { id, isClient, email, name, claims }); var result = new Dictionary<string, object?>
{
["id"] = id,
["email"] = email,
["isClient"] = isClient,
["isUser"] = !isClient,
["name"] = name,
["claims"] = claims
};
return JsValue.FromObject(engine, result);
} }
} }
} }

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

@ -5,104 +5,19 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class ScriptVars : ScriptContext public sealed class ScriptVars : ScriptContext
{ {
public ClaimsPrincipal? User
{
get => GetValue<ClaimsPrincipal?>();
set => SetValue(value);
}
public DomainId AppId
{
get => GetValue<DomainId>();
set => SetValue(value);
}
public DomainId SchemaId
{
get => GetValue<DomainId>();
set => SetValue(value);
}
public DomainId ContentId
{
get => GetValue<DomainId>();
set => SetValue(value);
}
public Status Status
{
get => GetValue<Status>();
set => SetValue(value);
}
public string? AppName
{
get => GetValue<string?>();
set => SetValue(value);
}
public string? SchemaName
{
get => GetValue<string?>();
set => SetValue(value);
}
public string? Operation
{
get => GetValue<string?>();
set => SetValue(value);
}
public ContentData? Data public ContentData? Data
{ {
get => GetValue<ContentData?>(); get => GetValue<ContentData?>();
set => SetValue(value); set => SetValue(value);
} }
#pragma warning disable CS0618 // Type or member is obsolete
public ContentData? DataOld
{
get => GetValue<ContentData?>();
set
{
SetValue(value, nameof(OldData));
SetValue(value);
}
}
public Status StatusOld
{
get => GetValue<Status>();
set
{
SetValue(value, nameof(OldStatus));
SetValue(value);
}
}
#pragma warning restore CS0618 // Type or member is obsolete
[Obsolete("Use dataOld")]
public ContentData? OldData
{
get => GetValue<ContentData?>();
}
[Obsolete("Use statusOld")]
public Status? OldStatus
{
get => GetValue<Status?>();
}
public void SetValue(object? value, [CallerMemberName] string? key = null) public void SetValue(object? value, [CallerMemberName] string? key = null)
{ {
if (key != null) if (key != null)

104
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompletion.cs

@ -1,104 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptingCompletion
{
private readonly Stack<string> prefixes = new Stack<string>();
private readonly HashSet<(string, string)> result = new HashSet<(string, string)>();
public IReadOnlyList<(string Name, string Description)> GetCompletion(Schema schema, PartitionResolver partitionResolver)
{
Push("ctx", "The context object holding all values.");
Add("appId", "The ID of the current app.");
Add("appName", "The name of the current app.");
Add("contentId", "The ID of the content item.");
Add("operation", "The currnet query operation.");
Add("status", "The status of the content item");
Add("statusOld", "The old status of the content item.");
Push("user", "Information about the current user.");
Add("id", "The ID of the user.");
Add("claims", "The additional properties of the user.");
Add("claims.key", "The additional property of the user with name 'key'.");
Add("claims['key']", "The additional property of the user with name 'key'.");
Add("email", "The email address of the current user.");
Add("isClient", "True when the current user is a client.");
Pop();
Push("data", "The data of the content item.");
AddData(schema, partitionResolver);
Pop();
Push("oldData", "The old data of the content item.");
AddData(schema, partitionResolver);
Pop();
Pop();
Add("replace()",
"Tell Squidex that you have modified the data and that the change should be applied.");
Add("disallow()",
"Tell Squidex to not allow the current operation and to return a 403 (Forbidden).");
Add("reject('Reason')",
"Tell Squidex to reject the current operation and to return a 403 (Forbidden).");
return result.OrderBy(x => x.Item1).ToList();
}
private void AddData(Schema schema, PartitionResolver partitionResolver)
{
foreach (var field in schema.Fields.Where(x => x.IsForApi(true)))
{
Push(field.Name, $"The values of the '{field.DisplayName()}' field.");
foreach (var partition in partitionResolver(field.Partitioning).AllKeys)
{
Push(partition, $"The '{partition}' value of the '{field.DisplayName()}' field.");
if (field is ArrayField arrayField)
{
foreach (var nestedField in arrayField.Fields.Where(x => x.IsForApi(true)))
{
Push(field.Name, $"The value of the '{nestedField.DisplayName()}' nested field.");
Pop();
}
}
Pop();
}
Pop();
}
}
private void Add(string name, string description)
{
result.Add((string.Join('.', prefixes.Reverse().Union(Enumerable.Repeat(name, 1))), description));
}
private void Push(string prefix, string description)
{
Add(prefix, description);
prefixes.Push(prefix);
}
private void Pop()
{
prefixes.Pop();
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs

@ -236,7 +236,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
Filter.Exists(x => x.LastModified), Filter.Exists(x => x.LastModified),
Filter.Exists(x => x.Id), Filter.Exists(x => x.Id),
Filter.Eq(x => x.IndexedAppId, appId), Filter.Eq(x => x.IndexedAppId, appId),
Filter.In(x => x.IndexedSchemaId, schemaIds), Filter.In(x => x.IndexedSchemaId, schemaIds)
}; };
if (query?.HasFilterField("dl") != true) if (query?.HasFilterField("dl") != true)

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs

@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
Index Index
.Ascending(x => x.AppId) .Ascending(x => x.AppId)
.Descending(x => x.Created) .Descending(x => x.Created)
.Descending(x => x.Version)), .Descending(x => x.Version))
}, ct); }, ct);
} }

16
backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureAssetScripts.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Assets;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class ConfigureAssetScripts : AppUpdateCommand
{
public AssetScripts? Scripts { get; set; }
}
}

13
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -41,6 +42,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
public AppContributors Contributors { get; set; } = AppContributors.Empty; public AppContributors Contributors { get; set; } = AppContributors.Empty;
public AssetScripts AssetScripts { get; set; } = AssetScripts.Empty;
public LanguagesConfig Languages { get; set; } = LanguagesConfig.English; public LanguagesConfig Languages { get; set; } = LanguagesConfig.English;
public Workflows Workflows { get; set; } = Workflows.Empty; public Workflows Workflows { get; set; } = Workflows.Empty;
@ -79,6 +82,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId): case AppPlanChanged e when Is.Change(Plan?.PlanId, e.PlanId):
return UpdatePlan(e.ToPlan()); return UpdatePlan(e.ToPlan());
case AppAssetsScriptsConfigured e when Is.Change(e.Scripts, AssetScripts):
return UpdateAssetScripts(e.Scripts);
case AppPlanReset e when Plan != null: case AppPlanReset e when Plan != null:
return UpdatePlan(null); return UpdatePlan(null);
@ -205,6 +211,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return true; return true;
} }
private bool UpdateAssetScripts(AssetScripts? scripts)
{
AssetScripts = scripts ?? AssetScripts.Empty;
return true;
}
private bool UpdateSettings(AppSettings settings) private bool UpdateSettings(AppSettings settings)
{ {
Settings = settings; Settings = settings;

15
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs

@ -113,6 +113,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return Snapshot; return Snapshot;
}); });
case ConfigureAssetScripts configureAssetScripts:
return UpdateReturn(configureAssetScripts, c =>
{
GuardApp.CanUpdateAssetScripts(c);
ConfigureAssetScripts(c);
return Snapshot;
});
case AssignContributor assignContributor: case AssignContributor assignContributor:
return UpdateReturnAsync(assignContributor, async c => return UpdateReturnAsync(assignContributor, async c =>
{ {
@ -345,6 +355,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
Raise(command, new AppSettingsUpdated()); Raise(command, new AppSettingsUpdated());
} }
private void ConfigureAssetScripts(ConfigureAssetScripts command)
{
Raise(command, new AppAssetsScriptsConfigured());
}
private void UpdateClient(UpdateClient command) private void UpdateClient(UpdateClient command)
{ {
Raise(command, new AppClientUpdated()); Raise(command, new AppClientUpdated());

7
backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -52,6 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
} }
public static void CanUpdateAssetScripts(ConfigureAssetScripts command)
{
Guard.NotNull(command, nameof(command));
}
public static void CanUpdateSettings(UpdateAppSettings command) public static void CanUpdateSettings(UpdateAppSettings command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));

3
backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Apps namespace Squidex.Domain.Apps.Entities.Apps
@ -34,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
AppContributors Contributors { get; } AppContributors Contributors { get; }
AssetScripts AssetScripts { get; }
LanguagesConfig Languages { get; } LanguagesConfig Languages { get; }
Workflows Workflows { get; } Workflows Workflows { get; }

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

@ -41,12 +41,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
private void AddAsset(ExecutionContext context) private void AddAsset(ExecutionContext context)
{ {
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId)) if (!context.TryGetValue<DomainId>("appId", out var appId))
{ {
return; return;
} }
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user)) if (!context.TryGetValue<ClaimsPrincipal>("user", out var user))
{ {
return; return;
} }

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public DomainId AssetId { get; set; } public DomainId AssetId { get; set; }
public bool DoNotScript { get; set; }
[IgnoreDataMember] [IgnoreDataMember]
public DomainId AggregateId public DomainId AggregateId
{ {

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public bool Duplicate { get; set; } public bool Duplicate { get; set; }
public bool OptimizeValidation { get; set; }
public CreateAsset() public CreateAsset()
{ {
AssetId = DomainId.NewGuid(); AssetId = DomainId.NewGuid();

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public DomainId ParentId { get; set; } public DomainId ParentId { get; set; }
public bool OptimizeValidation { get; set; }
public CreateAssetFolder() public CreateAssetFolder()
{ {
AssetFolderId = DomainId.NewGuid(); AssetFolderId = DomainId.NewGuid();

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs

@ -12,5 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public sealed class MoveAssetFolder : AssetFolderCommand public sealed class MoveAssetFolder : AssetFolderCommand
{ {
public DomainId ParentId { get; set; } public DomainId ParentId { get; set; }
public bool OptimizeValidation { get; set; }
} }
} }

123
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs

@ -6,12 +6,9 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; using Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -20,27 +17,19 @@ using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Log; using Squidex.Log;
using IAssetTagService = Squidex.Domain.Apps.Core.Tags.ITagService;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{ {
public sealed partial class AssetDomainObject : DomainObject<AssetDomainObject.State> public sealed partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
{ {
private readonly IContentRepository contentRepository; private readonly IServiceProvider serviceProvider;
private readonly IAssetTagService assetTags;
private readonly IAssetQueryService assetQuery;
public AssetDomainObject(IPersistenceFactory<AssetDomainObject.State> factory, ISemanticLog log, public AssetDomainObject(IPersistenceFactory<AssetDomainObject.State> factory, ISemanticLog log,
IAssetTagService assetTags, IServiceProvider serviceProvider)
IAssetQueryService assetQuery,
IContentRepository contentRepository)
: base(factory, log) : base(factory, log)
{ {
this.assetTags = assetTags;
this.assetQuery = assetQuery;
this.contentRepository = contentRepository;
Capacity = int.MaxValue; Capacity = int.MaxValue;
this.serviceProvider = serviceProvider;
} }
protected override bool IsDeleted() protected override bool IsDeleted()
@ -72,18 +61,20 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case UpsertAsset upsert: case UpsertAsset upsert:
return UpsertReturnAsync(upsert, async c => return UpsertReturnAsync(upsert, async c =>
{ {
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
if (Version > EtagVersion.Empty && !IsDeleted()) if (Version > EtagVersion.Empty && !IsDeleted())
{ {
UpdateCore(c.AsUpdate()); await UpdateCore(c.AsUpdate(), operation);
} }
else else
{ {
await CreateCore(c.AsCreate()); await CreateCore(c.AsCreate(), operation);
} }
if (Is.OptionalChange(Snapshot.ParentId, c.ParentId)) if (Is.OptionalChange(Snapshot.ParentId, c.ParentId))
{ {
await MoveCore(c.AsMove(c.ParentId.Value)); await MoveCore(c.AsMove(c.ParentId.Value), operation);
} }
return Snapshot; return Snapshot;
@ -92,11 +83,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case CreateAsset c: case CreateAsset c:
return CreateReturnAsync(c, async create => return CreateReturnAsync(c, async create =>
{ {
await CreateCore(create); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await CreateCore(create, operation);
if (Is.Change(Snapshot.ParentId, c.ParentId)) if (Is.Change(Snapshot.ParentId, c.ParentId))
{ {
await MoveCore(c.AsMove()); await MoveCore(c.AsMove(), operation);
} }
return Snapshot; return Snapshot;
@ -105,20 +98,19 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case AnnotateAsset c: case AnnotateAsset c:
return UpdateReturnAsync(c, async c => return UpdateReturnAsync(c, async c =>
{ {
if (c.Tags != null) var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
{
c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
}
Annotate(c); await AnnotateCore(c, operation);
return Snapshot; return Snapshot;
}); });
case UpdateAsset update: case UpdateAsset update:
return UpdateReturn(update, update => return UpdateReturnAsync(update, async c =>
{ {
Update(update); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await UpdateCore(c, operation);
return Snapshot; return Snapshot;
}); });
@ -126,7 +118,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case MoveAsset move: case MoveAsset move:
return UpdateReturnAsync(move, async c => return UpdateReturnAsync(move, async c =>
{ {
await MoveCore(c); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await MoveCore(c, operation);
return Snapshot; return Snapshot;
}); });
@ -134,55 +128,98 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case DeleteAsset delete when delete.Permanent: case DeleteAsset delete when delete.Permanent:
return DeletePermanentAsync(delete, async c => return DeletePermanentAsync(delete, async c =>
{ {
await DeleteCore(c); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation);
}); });
case DeleteAsset delete: case DeleteAsset delete:
return UpdateAsync(delete, async c => return UpdateAsync(delete, async c =>
{ {
await DeleteCore(c); var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation);
}); });
default: default:
throw new NotSupportedException(); throw new NotSupportedException();
} }
} }
private async Task CreateCore(CreateAsset create) private async Task CreateCore(CreateAsset create, AssetOperation operation)
{ {
if (!create.OptimizeValidation)
{
await operation.MustMoveToValidFolder(create.ParentId);
}
if (!create.DoNotScript)
{
await operation.ExecuteCreateScriptAsync(create);
}
if (create.Tags != null) if (create.Tags != null)
{ {
create.Tags = await NormalizeTagsAsync(create.AppId.Id, create.Tags); create.Tags = await operation.NormalizeTags(create.Tags);
} }
Create(create); Create(create);
} }
private void UpdateCore(UpdateAsset update) private async Task AnnotateCore(AnnotateAsset annotate, AssetOperation operation)
{ {
Update(update); if (!annotate.DoNotScript)
{
await operation.ExecuteAnnotateScriptAsync(annotate);
}
if (annotate.Tags != null)
{
annotate.Tags = await operation.NormalizeTags(annotate.Tags);
}
Annotate(annotate);
} }
private async Task MoveCore(MoveAsset move) private async Task UpdateCore(UpdateAsset update, AssetOperation operation)
{ {
await GuardAsset.CanMove(move, Snapshot, assetQuery); if (!update.DoNotScript)
{
await operation.ExecuteUpdateScriptAsync(update);
}
Move(move); Update(update);
} }
private async Task DeleteCore(DeleteAsset delete) private async Task MoveCore(MoveAsset move, AssetOperation operation)
{ {
await GuardAsset.CanDelete(delete, Snapshot, contentRepository); if (!move.OptimizeValidation)
{
await operation.MustMoveToValidFolder(move.ParentId);
}
await NormalizeTagsAsync(Snapshot.AppId.Id, null); if (!move.DoNotScript)
{
await operation.ExecuteMoveScriptAsync(move);
}
Delete(delete); Move(move);
} }
private async Task<HashSet<string>> NormalizeTagsAsync(DomainId appId, HashSet<string>? tags) private async Task DeleteCore(DeleteAsset delete, AssetOperation operation)
{ {
var normalized = await assetTags.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); if (delete.CheckReferrers)
{
await operation.CheckReferrersAsync();
}
return new HashSet<string>(normalized.Values); if (!delete.DoNotScript)
{
await operation.ExecuteDeleteScriptAsync(delete);
}
await operation.UnsetTags();
Delete(delete);
} }
private void Create(CreateAsset command) private void Create(CreateAsset command)

57
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -21,13 +21,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{ {
public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDomainObject.State> public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDomainObject.State>
{ {
private readonly IAssetQueryService assetQuery; private readonly IServiceProvider serviceProvider;
public AssetFolderDomainObject(IPersistenceFactory<State> factory, ISemanticLog log, public AssetFolderDomainObject(IPersistenceFactory<State> factory, ISemanticLog log,
IAssetQueryService assetQuery) IServiceProvider serviceProvider)
: base(factory, log) : base(factory, log)
{ {
this.assetQuery = assetQuery; this.serviceProvider = serviceProvider;
} }
protected override bool IsDeleted() protected override bool IsDeleted()
@ -54,9 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case CreateAssetFolder c: case CreateAssetFolder c:
return CreateReturnAsync(c, async create => return CreateReturnAsync(c, async create =>
{ {
await GuardAssetFolder.CanCreate(create, assetQuery); await CreateCore(create, c);
Create(create);
return Snapshot; return Snapshot;
}); });
@ -64,19 +62,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case MoveAssetFolder move: case MoveAssetFolder move:
return UpdateReturnAsync(move, async c => return UpdateReturnAsync(move, async c =>
{ {
await GuardAssetFolder.CanMove(c, Snapshot, assetQuery); await MoveCore(c);
Move(c);
return Snapshot; return Snapshot;
}); });
case RenameAssetFolder rename: case RenameAssetFolder rename:
return UpdateReturn(rename, c => return UpdateReturnAsync(rename, async c =>
{ {
GuardAssetFolder.CanRename(c); await RenameCore(c);
Rename(c);
return Snapshot; return Snapshot;
}); });
@ -92,6 +86,41 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
} }
} }
private async Task CreateCore(CreateAssetFolder create, CreateAssetFolder c)
{
var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHaveName(c.FolderName);
if (!c.OptimizeValidation)
{
await operation.MustMoveToValidFolder(c.ParentId);
}
Create(create);
}
private async Task MoveCore(MoveAssetFolder c)
{
var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot);
if (!c.OptimizeValidation)
{
await operation.MustMoveToValidFolder(c.ParentId);
}
Move(c);
}
private async Task RenameCore(RenameAssetFolder c)
{
var operation = await AssetFolderOperation.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHaveName(c.FolderName);
Rename(c);
}
private void Create(CreateAssetFolder command) private void Create(CreateAssetFolder command)
{ {
Raise(command, new AssetFolderCreated()); Raise(command, new AssetFolderCreated());

45
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderOperation.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{
public sealed class AssetFolderOperation : OperationContextBase<AssetFolderCommand, IAssetFolderEntity>
{
public AssetFolderOperation(IServiceProvider serviceProvider, Func<IAssetFolderEntity> snapshot)
: base(serviceProvider, snapshot)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
}
public static async Task<AssetFolderOperation> CreateAsync(IServiceProvider services, AssetFolderCommand command, Func<IAssetFolderEntity> snapshot)
{
var appProvider = services.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(command.AppId.Id);
if (app == null)
{
throw new DomainObjectNotFoundException(command.AppId.Id.ToString());
}
var id = command.AssetFolderId;
return new AssetFolderOperation(services, snapshot)
{
App = app,
Command = command,
CommandId = id
};
}
}
}

45
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetOperation.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{
public sealed class AssetOperation : OperationContextBase<AssetCommand, IAssetEntity>
{
public AssetOperation(IServiceProvider serviceProvider, Func<IAssetEntity> snapshot)
: base(serviceProvider, snapshot)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
}
public static async Task<AssetOperation> CreateAsync(IServiceProvider services, AssetCommand command, Func<IAssetEntity> snapshot)
{
var appProvider = services.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(command.AppId.Id);
if (app == null)
{
throw new DomainObjectNotFoundException(command.AppId.Id.ToString());
}
var id = command.AssetId;
return new AssetOperation(services, snapshot)
{
App = app,
Command = command,
CommandId = id
};
}
}
}

55
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAsset.cs

@ -1,55 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{
public static class GuardAsset
{
public static Task CanMove(MoveAsset command, IAssetEntity asset, IAssetQueryService assetQuery)
{
Guard.NotNull(command, nameof(command));
return Validate.It(async e =>
{
var parentId = command.ParentId;
if (parentId != asset.ParentId && parentId != DomainId.Empty && !command.OptimizeValidation)
{
var path = await assetQuery.FindAssetFolderAsync(command.AppId.Id, parentId);
if (path.Count == 0)
{
e(T.Get("assets.folderNotFound"), nameof(MoveAsset.ParentId));
}
}
});
}
public static async Task CanDelete(DeleteAsset command, IAssetEntity asset, IContentRepository contentRepository)
{
Guard.NotNull(command, nameof(command));
if (command.CheckReferrers)
{
var hasReferrer = await contentRepository.HasReferrersAsync(asset.AppId.Id, asset.Id, SearchScope.All, default);
if (hasReferrer)
{
throw new DomainException(T.Get("assets.referenced"), "OBJECT_REFERENCED");
}
}
}
}
}

82
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/GuardAssetFolder.cs

@ -1,82 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{
public static class GuardAssetFolder
{
public static Task CanCreate(CreateAssetFolder command, IAssetQueryService assetQuery)
{
Guard.NotNull(command, nameof(command));
return Validate.It(async e =>
{
if (string.IsNullOrWhiteSpace(command.FolderName))
{
e(Not.Defined(nameof(command.FolderName)), nameof(command.FolderName));
}
await CheckPathAsync(command.AppId.Id, command.ParentId, assetQuery, DomainId.Empty, e);
});
}
public static void CanRename(RenameAssetFolder command)
{
Guard.NotNull(command, nameof(command));
Validate.It(e =>
{
if (string.IsNullOrWhiteSpace(command.FolderName))
{
e(Not.Defined(nameof(command.FolderName)), nameof(command.FolderName));
}
});
}
public static Task CanMove(MoveAssetFolder command, IAssetFolderEntity assetFolder, IAssetQueryService assetQuery)
{
Guard.NotNull(command, nameof(command));
return Validate.It(async e =>
{
if (command.ParentId != assetFolder.ParentId)
{
await CheckPathAsync(command.AppId.Id, command.ParentId, assetQuery, assetFolder.Id, e);
}
});
}
private static async Task CheckPathAsync(DomainId appId, DomainId parentId, IAssetQueryService assetQuery, DomainId id, AddValidation e)
{
if (parentId != DomainId.Empty)
{
var path = await assetQuery.FindAssetFolderAsync(appId, parentId);
if (path.Count == 0)
{
e(T.Get("assets.folderNotFound"), nameof(MoveAssetFolder.ParentId));
}
else if (id != DomainId.Empty)
{
var indexOfSelf = path.IndexOf(x => x.Id == id);
var indexOfParent = path.IndexOf(x => x.Id == parentId);
if (indexOfSelf >= 0 && indexOfParent > indexOfSelf)
{
e(T.Get("assets.folderRecursion"), nameof(MoveAssetFolder.ParentId));
}
}
}
}
}
}

128
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptMetadataWrapper.cs

@ -0,0 +1,128 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{
public sealed class ScriptMetadataWrapper : 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.Select(x => (object?)x).ToList();
}
public object? this[string key]
{
get => metadata[key];
set => metadata[key] = JsonValue.Create(value);
}
public bool IsReadOnly
{
get => false;
}
public ScriptMetadataWrapper(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();
}
}
}

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

@ -0,0 +1,265 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
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
{
public static class ScriptingExtensions
{
private static readonly ScriptOptions Options = new ScriptOptions
{
AsContext = true,
CanDisallow = true,
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;
if (string.IsNullOrWhiteSpace(script))
{
return;
}
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
{
// 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
},
[ScriptKeys.Operation] = "Create"
};
await ExecuteScriptAsync(operation, script, vars);
}
public static Task ExecuteUpdateScriptAsync(this AssetOperation operation, UpdateAsset update)
{
var script = operation.App.AssetScripts?.Update;
if (string.IsNullOrWhiteSpace(script))
{
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?>
{
[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
},
[ScriptKeys.Operation] = "Update"
};
return ExecuteScriptAsync(operation, script, vars);
}
public static Task ExecuteAnnotateScriptAsync(this AssetOperation operation, AnnotateAsset annotate)
{
var script = operation.App.AssetScripts?.Annotate;
if (string.IsNullOrWhiteSpace(script))
{
return Task.CompletedTask;
}
// Tags 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?>
{
[ScriptKeys.Metadata] = annotate.Metadata?.Mutable(),
[ScriptKeys.FileName] = annotate.FileName,
[ScriptKeys.FileSlug] = annotate.Slug,
[ScriptKeys.Tags] = annotate.Tags
},
[ScriptKeys.Operation] = "Annotate"
};
return ExecuteScriptAsync(operation, script, vars);
}
public static async Task ExecuteMoveScriptAsync(this AssetOperation operation, MoveAsset move)
{
var script = operation.App.AssetScripts?.Move;
if (string.IsNullOrWhiteSpace(script))
{
return;
}
var parentPath = await GetPathAsync(operation, move.ParentId);
var vars = new ScriptVars
{
// Use a dictionary for better performance, because no reflection is involved.
[ScriptKeys.Command] = new Dictionary<string, object?>
{
[ScriptKeys.ParentId] = move.ParentId,
[ScriptKeys.ParentPath] = parentPath
},
[ScriptKeys.Operation] = "Move"
};
await ExecuteScriptAsync(operation, script, vars);
}
public static Task ExecuteDeleteScriptAsync(this AssetOperation operation, DeleteAsset delete)
{
var script = operation.App.AssetScripts?.Delete;
if (string.IsNullOrWhiteSpace(script))
{
return Task.CompletedTask;
}
var vars = new ScriptVars
{
// Use a dictionary for better performance, because no reflection is involved.
[ScriptKeys.Command] = new Dictionary<string, object?>
{
[ScriptKeys.Permanent] = delete.Permanent
},
[ScriptKeys.Operation] = "Delete"
};
return ExecuteScriptAsync(operation, script, vars);
}
private static async Task ExecuteScriptAsync(AssetOperation operation, string script, ScriptVars 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()
};
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;
var scriptEngine = operation.Resolve<IScriptEngine>();
await scriptEngine.ExecuteAsync(vars, script, Options);
}
private static async Task<object> GetPathAsync(AssetOperation operation, DomainId parentId)
{
if (parentId == default)
{
return Array.Empty<object>();
}
var assetQuery = operation.Resolve<IAssetQueryService>();
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());
}
}
}

32
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/TagsExtensions.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{
public static class TagService
{
public static async Task<HashSet<string>> NormalizeTags(this AssetOperation operation, HashSet<string> tags)
{
var tagService = operation.Resolve<ITagService>();
var normalized = await tagService.NormalizeTagsAsync(operation.App.Id, TagGroups.Assets, tags, operation.Snapshot.Tags);
return new HashSet<string>(normalized.Values);
}
public static async Task UnsetTags(this AssetOperation operation)
{
var tagService = operation.Resolve<ITagService>();
await tagService.NormalizeTagsAsync(operation.App.Id, TagGroups.Assets, null, operation.Snapshot.Tags);
}
}
}

93
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ValidationExtensions.cs

@ -0,0 +1,93 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{
public static class ValidationExtensions
{
public static void MustHaveName(this AssetFolderOperation operation, string folderName)
{
if (string.IsNullOrWhiteSpace(folderName))
{
operation.AddError(Not.Defined(nameof(folderName)), "FolderName");
}
operation.ThrowOnErrors();
}
public static async Task MustMoveToValidFolder(this AssetOperation operation, DomainId parentId)
{
// If moved to root folder or not moved at all, we can just skip the validation.
if (parentId == DomainId.Empty || parentId == operation.Snapshot.ParentId)
{
return;
}
var assetQuery = operation.Resolve<IAssetQueryService>();
var path = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId);
if (path.Count == 0)
{
operation.AddError(T.Get("assets.folderNotFound"), nameof(MoveAsset.ParentId)).ThrowOnErrors();
}
operation.ThrowOnErrors();
}
public static async Task MustMoveToValidFolder(this AssetFolderOperation operation, DomainId parentId)
{
// If moved to root folder or not moved at all, we can just skip the validation.
if (parentId == DomainId.Empty || parentId == operation.Snapshot.ParentId)
{
return;
}
var assetQuery = operation.Resolve<IAssetQueryService>();
var path = await assetQuery.FindAssetFolderAsync(operation.App.Id, parentId);
if (path.Count == 0)
{
operation.AddError(T.Get("assets.folderNotFound"), nameof(MoveAssetFolder.ParentId));
}
else if (operation.CommandId != DomainId.Empty)
{
var indexOfSelf = path.IndexOf(x => x.Id == operation.CommandId);
var indexOfParent = path.IndexOf(x => x.Id == parentId);
// If we would move the folder to its own parent (the parent comes first in the path), we would create a recursion.
if (indexOfSelf >= 0 && indexOfParent > indexOfSelf)
{
operation.AddError(T.Get("assets.folderRecursion"), nameof(MoveAssetFolder.ParentId));
}
}
operation.ThrowOnErrors();
}
public static async Task CheckReferrersAsync(this AssetOperation operation)
{
var contentRepository = operation.Resolve<IContentRepository>();
var hasReferrer = await contentRepository.HasReferrersAsync(operation.App.Id, operation.CommandId, SearchScope.All, default);
if (hasReferrer)
{
throw new DomainException(T.Get("assets.referenced"), "OBJECT_REFERENCED");
}
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
public void Extend(ExecutionContext context) public void Extend(ExecutionContext context)
{ {
if (context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId)) if (context.TryGetValue<DomainId>("appId", out var appId))
{ {
var engine = context.Engine; var engine = context.Engine;

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

@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case UpsertContent upsertContent: case UpsertContent upsertContent:
return UpsertReturnAsync(upsertContent, async c => return UpsertReturnAsync(upsertContent, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
if (Version > EtagVersion.Empty && !IsDeleted()) if (Version > EtagVersion.Empty && !IsDeleted())
{ {
@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
await CreateCore(c.AsCreate(), operation); await CreateCore(c.AsCreate(), operation);
} }
if (Is.OptionalChange(operation.Content.EditingStatus(), c.Status)) if (Is.OptionalChange(operation.Snapshot.EditingStatus(), c.Status))
{ {
await ChangeCore(c.AsChange(c.Status.Value), operation); await ChangeCore(c.AsChange(c.Status.Value), operation);
} }
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case CreateContent createContent: case CreateContent createContent:
return CreateReturnAsync(createContent, async c => return CreateReturnAsync(createContent, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await CreateCore(c, operation); await CreateCore(c, operation);
@ -109,11 +109,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case ValidateContent validate: case ValidateContent validate:
return UpdateReturnAsync(validate, async c => return UpdateReturnAsync(validate, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHavePermission(Permissions.AppContentsReadOwn); await ValidateCore(operation);
await operation.ValidateContentAndInputAsync(Snapshot.Data, false, Snapshot.IsPublished());
return true; return true;
}); });
@ -121,14 +119,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case CreateContentDraft createDraft: case CreateContentDraft createDraft:
return UpdateReturnAsync(createDraft, async c => return UpdateReturnAsync(createDraft, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHavePermission(Permissions.AppContentsVersionCreate);
operation.MustCreateDraft();
var status = await operation.GetInitialStatusAsync(); await CreateDraftCore(c, operation);
CreateDraft(c, status);
return Snapshot; return Snapshot;
}); });
@ -136,12 +129,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case DeleteContentDraft deleteDraft: case DeleteContentDraft deleteDraft:
return UpdateReturnAsync(deleteDraft, async c => return UpdateReturnAsync(deleteDraft, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHavePermission(Permissions.AppContentsVersionDelete);
operation.MustDeleteDraft();
DeleteDraft(c); DeleteDraftCore(c, operation);
return Snapshot; return Snapshot;
}); });
@ -149,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case PatchContent patchContent: case PatchContent patchContent:
return UpdateReturnAsync(patchContent, async c => return UpdateReturnAsync(patchContent, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await PatchCore(c, operation); await PatchCore(c, operation);
@ -159,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case UpdateContent updateContent: case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, async c => return UpdateReturnAsync(updateContent, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await UpdateCore(c, operation); await UpdateCore(c, operation);
@ -169,14 +159,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case CancelContentSchedule cancelContentSchedule: case CancelContentSchedule cancelContentSchedule:
return UpdateReturnAsync(cancelContentSchedule, async c => return UpdateReturnAsync(cancelContentSchedule, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHavePermission(Permissions.AppContentsChangeStatusCancel); CancelChangeCore(c, operation);
if (Snapshot.ScheduleJob != null)
{
CancelChangeStatus(c);
}
return Snapshot; return Snapshot;
}); });
@ -192,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
} }
else else
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await ChangeCore(c, operation); await ChangeCore(c, operation);
} }
@ -215,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case DeleteContent deleteContent when deleteContent.Permanent: case DeleteContent deleteContent when deleteContent.Permanent:
return DeletePermanentAsync(deleteContent, async c => return DeletePermanentAsync(deleteContent, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation); await DeleteCore(c, operation);
}); });
@ -223,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case DeleteContent deleteContent: case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c => return UpdateAsync(deleteContent, async c =>
{ {
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot); var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation); await DeleteCore(c, operation);
}); });
@ -233,7 +218,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
} }
} }
private async Task CreateCore(CreateContent c, OperationContext operation) private async Task CreateCore(CreateContent c, ContentOperation operation)
{ {
operation.MustNotCreateSingleton(); operation.MustNotCreateSingleton();
operation.MustNotCreateForUnpublishedSchema(); operation.MustNotCreateForUnpublishedSchema();
@ -261,7 +246,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Create(c, status); Create(c, status);
} }
private async Task ChangeCore(ChangeContentStatus c, OperationContext operation) private async Task ChangeCore(ChangeContentStatus c, ContentOperation operation)
{ {
operation.MustHavePermission(Permissions.AppContentsChangeStatusOwn); operation.MustHavePermission(Permissions.AppContentsChangeStatusOwn);
operation.MustNotChangeSingleton(c.Status); operation.MustNotChangeSingleton(c.Status);
@ -314,7 +299,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
ChangeStatus(c); ChangeStatus(c);
} }
private async Task UpdateCore(UpdateContent c, OperationContext operation) private async Task UpdateCore(UpdateContent c, ContentOperation operation)
{ {
operation.MustHavePermission(Permissions.AppContentsUpdate); operation.MustHavePermission(Permissions.AppContentsUpdate);
operation.MustHaveData(c.Data); operation.MustHaveData(c.Data);
@ -349,7 +334,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Update(c, newData); Update(c, newData);
} }
private async Task PatchCore(UpdateContent c, OperationContext operation) private async Task PatchCore(UpdateContent c, ContentOperation operation)
{ {
operation.MustHavePermission(Permissions.AppContentsUpdate); operation.MustHavePermission(Permissions.AppContentsUpdate);
operation.MustHaveData(c.Data); operation.MustHaveData(c.Data);
@ -384,7 +369,42 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Update(c, newData); Update(c, newData);
} }
private async Task DeleteCore(DeleteContent c, OperationContext operation) private void CancelChangeCore(CancelContentSchedule c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsChangeStatusCancel);
if (Snapshot.ScheduleJob != null)
{
CancelChangeStatus(c);
}
}
private async Task ValidateCore(ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsReadOwn);
await operation.ValidateContentAndInputAsync(Snapshot.Data, false, Snapshot.IsPublished());
}
private async Task CreateDraftCore(CreateContentDraft c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsVersionCreate);
operation.MustCreateDraft();
var status = await operation.GetInitialStatusAsync();
CreateDraft(c, status);
}
private void DeleteDraftCore(DeleteContentDraft c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsVersionDelete);
operation.MustDeleteDraft();
DeleteDraft(c);
}
private async Task DeleteCore(DeleteContent c, ContentOperation operation)
{ {
operation.MustHavePermission(Permissions.AppContentsDeleteOwn); operation.MustHavePermission(Permissions.AppContentsDeleteOwn);
operation.MustNotDeleteSingleton(); operation.MustNotDeleteSingleton();

62
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentOperation.cs

@ -0,0 +1,62 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
public sealed class ContentOperation : OperationContextBase<ContentCommand, IContentEntity>
{
public ISchemaEntity Schema { get; init; }
public ResolvedComponents Components { get; init; }
public Schema SchemaDef
{
get => Schema.SchemaDef;
}
public ContentOperation(IServiceProvider serviceProvider, Func<IContentEntity> snapshot)
: base(serviceProvider, snapshot)
{
}
public static async Task<ContentOperation> CreateAsync(IServiceProvider services, ContentCommand command, Func<IContentEntity> snapshot)
{
var appProvider = services.GetRequiredService<IAppProvider>();
var (app, schema) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id);
if (app == null)
{
throw new DomainObjectNotFoundException(command.AppId.Id.ToString());
}
if (schema == null)
{
throw new DomainObjectNotFoundException(command.SchemaId.Id.ToString());
}
var components = await appProvider.GetComponentsAsync(schema);
return new ContentOperation(services, snapshot)
{
App = app,
Command = command,
CommandId = command.ContentId,
Components = components,
Schema = schema
};
}
}
}

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

@ -20,103 +20,140 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
CanReject = true CanReject = true
}; };
public static async Task<ContentData> ExecuteCreateScriptAsync(this OperationContext context, ContentData data, Status status) private static class ScriptKeys
{ {
var script = context.SchemaDef.Scripts.Create; 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;
if (!string.IsNullOrWhiteSpace(script)) if (string.IsNullOrWhiteSpace(script))
{ {
var vars = Enrich(context, new ScriptVars return Task.FromResult(data);
{
Operation = "Create",
Data = data,
DataOld = default,
Status = status,
StatusOld = default
});
data = await GetScriptEngine(context).TransformAsync(vars, script, Options);
} }
return data; var vars = Enrich(operation, new ScriptVars
{
[ScriptKeys.Data] = data,
[ScriptKeys.DataOld] = null,
[ScriptKeys.OldData] = null,
[ScriptKeys.OldStatus] = default(Status),
[ScriptKeys.Operation] = "Create",
[ScriptKeys.Status] = status,
[ScriptKeys.StatusOld] = default(Status)
});
return TransformAsync(operation, script, vars);
} }
public static async Task<ContentData> ExecuteUpdateScriptAsync(this OperationContext context, ContentData data) public static Task<ContentData> ExecuteUpdateScriptAsync(this ContentOperation operation, ContentData data)
{ {
var script = context.SchemaDef.Scripts.Update; var script = operation.SchemaDef.Scripts.Update;
if (!string.IsNullOrWhiteSpace(script)) if (string.IsNullOrWhiteSpace(script))
{ {
var vars = Enrich(context, new ScriptVars return Task.FromResult(data);
{
Operation = "Update",
Data = data,
DataOld = context.Content.Data,
Status = context.Content.EditingStatus(),
StatusOld = default
});
data = await GetScriptEngine(context).TransformAsync(vars, script, Options);
} }
return data; var vars = Enrich(operation, new ScriptVars
{
[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)
});
return TransformAsync(operation, script, vars);
} }
public static async Task<ContentData> ExecuteChangeScriptAsync(this OperationContext context, Status status, StatusChange change) public static Task<ContentData> ExecuteChangeScriptAsync(this ContentOperation operation, Status status, StatusChange change)
{ {
var script = context.SchemaDef.Scripts.Change; var script = operation.SchemaDef.Scripts.Change;
if (!string.IsNullOrWhiteSpace(script)) if (string.IsNullOrWhiteSpace(script))
{ {
var data = context.Content.Data.Clone(); return Task.FromResult(operation.Snapshot.Data);
var vars = Enrich(context, new ScriptVars
{
Operation = change.ToString(),
Data = data,
DataOld = default,
Status = status,
StatusOld = context.Content.EditingStatus()
});
return await GetScriptEngine(context).TransformAsync(vars, script, Options);
} }
return context.Content.Data; // Clone the data so we do not change it by accident.
var data = operation.Snapshot.Data.Clone();
var vars = Enrich(operation, new ScriptVars
{
[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()
});
return TransformAsync(operation, script, vars);
} }
public static async Task ExecuteDeleteScriptAsync(this OperationContext context) public static Task ExecuteDeleteScriptAsync(this ContentOperation operation)
{ {
var script = context.SchemaDef.Scripts.Delete; var script = operation.SchemaDef.Scripts.Delete;
if (!string.IsNullOrWhiteSpace(script)) if (string.IsNullOrWhiteSpace(script))
{ {
var vars = Enrich(context, new ScriptVars return Task.CompletedTask;
{
Operation = "Delete",
Data = context.Content.Data,
DataOld = default,
Status = context.Content.EditingStatus(),
StatusOld = default
});
await GetScriptEngine(context).ExecuteAsync(vars, script, Options);
} }
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)
});
return ExecuteAsync(operation, script, vars);
}
private static async Task<ContentData> TransformAsync(ContentOperation operation, string script, ScriptVars vars)
{
return await operation.Resolve<IScriptEngine>().TransformAsync(vars, script, Options);
} }
private static IScriptEngine GetScriptEngine(OperationContext context) private static async Task ExecuteAsync(ContentOperation operation, string script, ScriptVars vars)
{ {
return context.Resolve<IScriptEngine>(); await operation.Resolve<IScriptEngine>().ExecuteAsync(vars, script, Options);
} }
private static ScriptVars Enrich(OperationContext context, ScriptVars vars) private static ScriptVars Enrich(ContentOperation operation, ScriptVars vars)
{ {
vars.ContentId = context.ContentId; vars[ScriptKeys.AppId] = operation.App.Id;
vars.AppId = context.App.Id; vars[ScriptKeys.AppName] = operation.App.Name;
vars.AppName = context.App.Name; vars[ScriptKeys.Command] = operation.Command;
vars.SchemaId = context.Schema.Id; vars[ScriptKeys.Content] = operation.Snapshot;
vars.SchemaName = context.Schema.SchemaDef.Name; vars[ScriptKeys.ContentId] = operation.CommandId;
vars.User = context.User; vars[ScriptKeys.SchemaId] = operation.Schema.Id;
vars[ScriptKeys.SchemaName] = operation.Schema.SchemaDef.Name;
vars[ScriptKeys.User] = operation.User;
return vars; return vars;
} }

4
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs

@ -14,9 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
public static class SecurityExtensions public static class SecurityExtensions
{ {
public static void MustHavePermission(this OperationContext context, string permissionId) public static void MustHavePermission(this ContentOperation context, string permissionId)
{ {
var content = context.Content; var content = context.Snapshot;
if (Equals(content.CreatedBy, context.Actor) || context.User == null) if (Equals(content.CreatedBy, context.Actor) || context.User == null)
{ {

16
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SingletonExtensions.cs

@ -14,33 +14,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
public static class SingletonExtensions public static class SingletonExtensions
{ {
public static void MustNotCreateForUnpublishedSchema(this OperationContext context) public static void MustNotCreateForUnpublishedSchema(this ContentOperation operation)
{ {
if (!context.SchemaDef.IsPublished && context.SchemaDef.Type != SchemaType.Singleton) if (!operation.SchemaDef.IsPublished && operation.SchemaDef.Type != SchemaType.Singleton)
{ {
throw new DomainException(T.Get("contents.schemaNotPublished")); throw new DomainException(T.Get("contents.schemaNotPublished"));
} }
} }
public static void MustNotCreateSingleton(this OperationContext context) public static void MustNotCreateSingleton(this ContentOperation operation)
{ {
if (context.SchemaDef.Type == SchemaType.Singleton && context.ContentId != context.Schema.Id) if (operation.SchemaDef.Type == SchemaType.Singleton && operation.CommandId != operation.Schema.Id)
{ {
throw new DomainException(T.Get("contents.singletonNotCreatable")); throw new DomainException(T.Get("contents.singletonNotCreatable"));
} }
} }
public static void MustNotChangeSingleton(this OperationContext context, Status status) public static void MustNotChangeSingleton(this ContentOperation operation, Status status)
{ {
if (context.SchemaDef.Type == SchemaType.Singleton && (context.Content.NewStatus == null || status != Status.Published)) if (operation.SchemaDef.Type == SchemaType.Singleton && (operation.Snapshot.NewStatus == null || status != Status.Published))
{ {
throw new DomainException(T.Get("contents.singletonNotChangeable")); throw new DomainException(T.Get("contents.singletonNotChangeable"));
} }
} }
public static void MustNotDeleteSingleton(this OperationContext context) public static void MustNotDeleteSingleton(this ContentOperation operation)
{ {
if (context.SchemaDef.Type == SchemaType.Singleton) if (operation.SchemaDef.Type == SchemaType.Singleton)
{ {
throw new DomainException(T.Get("contents.singletonNotDeletable")); throw new DomainException(T.Get("contents.singletonNotDeletable"));
} }

68
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs

@ -24,58 +24,60 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
public static class ValidationExtensions public static class ValidationExtensions
{ {
public static void MustDeleteDraft(this OperationContext context) public static void MustDeleteDraft(this ContentOperation operation)
{ {
if (context.Content.NewStatus == null) if (operation.Snapshot.NewStatus == null)
{ {
throw new DomainException(T.Get("contents.draftToDeleteNotFound")); throw new DomainException(T.Get("contents.draftToDeleteNotFound"));
} }
} }
public static void MustCreateDraft(this OperationContext context) public static void MustCreateDraft(this ContentOperation operation)
{ {
if (context.Content.EditingStatus() != Status.Published) if (operation.Snapshot.EditingStatus() != Status.Published)
{ {
throw new DomainException(T.Get("contents.draftNotCreateForUnpublished")); throw new DomainException(T.Get("contents.draftNotCreateForUnpublished"));
} }
} }
public static void MustHaveData(this OperationContext context, ContentData? data) public static void MustHaveData(this ContentOperation operation, ContentData? data)
{ {
if (data == null) if (data == null)
{ {
context.AddError(Not.Defined(nameof(data)), nameof(data)).ThrowOnErrors(); operation.AddError(Not.Defined(nameof(data)), nameof(data));
} }
operation.ThrowOnErrors();
} }
public static async Task ValidateInputAsync(this OperationContext context, ContentData data, bool optimize, bool published) public static async Task ValidateInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published)
{ {
var validator = GetValidator(context, optimize, published); var validator = GetValidator(operation, optimize, published);
await validator.ValidateInputAsync(data); await validator.ValidateInputAsync(data);
context.AddErrors(validator.Errors).ThrowOnErrors(); operation.AddErrors(validator.Errors).ThrowOnErrors();
} }
public static async Task ValidateInputPartialAsync(this OperationContext context, ContentData data, bool optimize, bool published) public static async Task ValidateInputPartialAsync(this ContentOperation operation, ContentData data, bool optimize, bool published)
{ {
var validator = GetValidator(context, optimize, published); var validator = GetValidator(operation, optimize, published);
await validator.ValidateInputPartialAsync(data); await validator.ValidateInputPartialAsync(data);
context.AddErrors(validator.Errors).ThrowOnErrors(); operation.AddErrors(validator.Errors).ThrowOnErrors();
} }
public static async Task ValidateContentAsync(this OperationContext context, ContentData data, bool optimize, bool published) public static async Task ValidateContentAsync(this ContentOperation operation, ContentData data, bool optimize, bool published)
{ {
var validator = GetValidator(context, optimize, published); var validator = GetValidator(operation, optimize, published);
await validator.ValidateContentAsync(data); await validator.ValidateContentAsync(data);
context.AddErrors(validator.Errors).ThrowOnErrors(); operation.AddErrors(validator.Errors).ThrowOnErrors();
} }
public static async Task ValidateContentAndInputAsync(this OperationContext operation, ContentData data, bool optimize, bool published) public static async Task ValidateContentAndInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published)
{ {
var validator = GetValidator(operation, optimize, published); var validator = GetValidator(operation, optimize, published);
@ -85,16 +87,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
operation.AddErrors(validator.Errors).ThrowOnErrors(); operation.AddErrors(validator.Errors).ThrowOnErrors();
} }
public static void GenerateDefaultValues(this OperationContext context, ContentData data) public static void GenerateDefaultValues(this ContentOperation operation, ContentData data)
{ {
data.GenerateDefaultValues(context.Schema.SchemaDef, context.Partition()); data.GenerateDefaultValues(operation.Schema.SchemaDef, operation.Partition());
} }
public static async Task CheckReferrersAsync(this OperationContext context) public static async Task CheckReferrersAsync(this ContentOperation operation)
{ {
var contentRepository = context.Resolve<IContentRepository>(); var contentRepository = operation.Resolve<IContentRepository>();
var hasReferrer = await contentRepository.HasReferrersAsync(context.App.Id, context.ContentId, SearchScope.All, default); var hasReferrer = await contentRepository.HasReferrersAsync(operation.App.Id, operation.CommandId, SearchScope.All, default);
if (hasReferrer) if (hasReferrer)
{ {
@ -102,29 +104,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
} }
} }
private static ContentValidator GetValidator(this OperationContext context, bool optimize, bool published) private static ContentValidator GetValidator(this ContentOperation operation, bool optimize, bool published)
{ {
var validationContext = var validationContext =
new ValidationContext(context.Resolve<IJsonSerializer>(), new ValidationContext(operation.Resolve<IJsonSerializer>(),
context.App.NamedId(), operation.App.NamedId(),
context.Schema.NamedId(), operation.Schema.NamedId(),
context.SchemaDef, operation.SchemaDef,
context.Components, operation.Components,
context.ContentId) operation.CommandId)
.Optimized(optimize).AsPublishing(published); .Optimized(optimize).AsPublishing(published);
var validator = var validator =
new ContentValidator(context.Partition(), new ContentValidator(operation.Partition(),
validationContext, validationContext,
context.Resolve<IEnumerable<IValidatorsFactory>>(), operation.Resolve<IEnumerable<IValidatorsFactory>>(),
context.Resolve<ISemanticLog>()); operation.Resolve<ISemanticLog>());
return validator; return validator;
} }
private static PartitionResolver Partition(this OperationContext context) private static PartitionResolver Partition(this ContentOperation operation)
{ {
return context.App.PartitionResolver(); return operation.App.PartitionResolver();
} }
} }
} }

46
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs

@ -15,65 +15,65 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
public static class WorkflowExtensions public static class WorkflowExtensions
{ {
public static Task<Status> GetInitialStatusAsync(this OperationContext context) public static Task<Status> GetInitialStatusAsync(this ContentOperation operation)
{ {
var workflow = GetWorkflow(context); var workflow = GetWorkflow(operation);
return workflow.GetInitialStatusAsync(context.Schema); return workflow.GetInitialStatusAsync(operation.Schema);
} }
public static async Task CheckTransitionAsync(this OperationContext context, Status status) public static async Task CheckTransitionAsync(this ContentOperation operation, Status status)
{ {
if (context.SchemaDef.Type != SchemaType.Singleton) if (operation.SchemaDef.Type != SchemaType.Singleton)
{ {
var workflow = GetWorkflow(context); var workflow = GetWorkflow(operation);
var oldStatus = context.Content.EditingStatus(); var oldStatus = operation.Snapshot.EditingStatus();
if (!await workflow.CanMoveToAsync(context.Content, oldStatus, status, context.User)) if (!await workflow.CanMoveToAsync(operation.Snapshot, oldStatus, status, operation.User))
{ {
var values = new { oldStatus, newStatus = status }; var values = new { oldStatus, newStatus = status };
context.AddError(T.Get("contents.statusTransitionNotAllowed", values), nameof(status)); operation.AddError(T.Get("contents.statusTransitionNotAllowed", values), nameof(status));
context.ThrowOnErrors(); operation.ThrowOnErrors();
} }
} }
} }
public static async Task CheckStatusAsync(this OperationContext context, Status status) public static async Task CheckStatusAsync(this ContentOperation operation, Status status)
{ {
if (context.SchemaDef.Type != SchemaType.Singleton) if (operation.SchemaDef.Type != SchemaType.Singleton)
{ {
var workflow = GetWorkflow(context); var workflow = GetWorkflow(operation);
var statusInfo = await workflow.GetInfoAsync(context.Content, status); var statusInfo = await workflow.GetInfoAsync(operation.Snapshot, status);
if (statusInfo == null) if (statusInfo == null)
{ {
context.AddError(T.Get("contents.statusNotValid"), nameof(status)); operation.AddError(T.Get("contents.statusNotValid"), nameof(status));
context.ThrowOnErrors(); operation.ThrowOnErrors();
} }
} }
} }
public static async Task CheckUpdateAsync(this OperationContext context) public static async Task CheckUpdateAsync(this ContentOperation operation)
{ {
if (context.User != null) if (operation.User != null)
{ {
var workflow = GetWorkflow(context); var workflow = GetWorkflow(operation);
var status = context.Content.EditingStatus(); var status = operation.Snapshot.EditingStatus();
if (!await workflow.CanUpdateAsync(context.Content, status, context.User)) if (!await workflow.CanUpdateAsync(operation.Snapshot, status, operation.User))
{ {
throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status }));
} }
} }
} }
private static IContentWorkflow GetWorkflow(OperationContext context) private static IContentWorkflow GetWorkflow(ContentOperation operation)
{ {
return context.Resolve<IContentWorkflow>(); return operation.Resolve<IContentWorkflow>();
} }
} }
} }

127
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/OperationContext.cs

@ -1,127 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
public sealed class OperationContext
{
private readonly List<ValidationError> errors = new List<ValidationError>();
private readonly IServiceProvider serviceProvider;
public ClaimsPrincipal? User { get; init; }
public RefToken Actor { get; init; }
public IAppEntity App { get; init; }
public ISchemaEntity Schema { get; init; }
public DomainId ContentId { get; init; }
public ResolvedComponents Components { get; init; }
public Func<IContentEntity> ContentProvider { get; init; }
public IContentEntity Content
{
get => ContentProvider();
}
public Schema SchemaDef
{
get => Schema.SchemaDef;
}
public OperationContext(IServiceProvider serviceProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.serviceProvider = serviceProvider;
}
public static async Task<OperationContext> CreateAsync(IServiceProvider services, ContentCommand command, Func<IContentEntity> snapshot)
{
var appProvider = services.GetRequiredService<IAppProvider>();
var (app, schema) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id);
if (app == null)
{
throw new DomainObjectNotFoundException(command.AppId.Id.ToString());
}
if (schema == null)
{
throw new DomainObjectNotFoundException(command.SchemaId.Id.ToString());
}
var components = await appProvider.GetComponentsAsync(schema);
return new OperationContext(services)
{
App = app,
Actor = command.Actor,
Components = components,
ContentProvider = snapshot,
ContentId = command.ContentId,
Schema = schema,
User = command.User
};
}
public T Resolve<T>() where T : notnull
{
return serviceProvider.GetRequiredService<T>();
}
public T? ResolveOptional<T>() where T : class
{
return serviceProvider.GetService(typeof(T)) as T;
}
public OperationContext AddError(string message, params string[] propertyNames)
{
errors.Add(new ValidationError(message, propertyNames));
return this;
}
public OperationContext AddError(ValidationError newError)
{
errors.Add(newError);
return this;
}
public OperationContext AddErrors(IEnumerable<ValidationError> newErrors)
{
errors.AddRange(newErrors);
return this;
}
public void ThrowOnErrors()
{
if (errors.Count > 0)
{
throw new ValidationException(errors);
}
}
}
}

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs

@ -8,6 +8,7 @@
using GraphQL; using GraphQL;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -22,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "path", Name = "path",
Description = "The path to the json value", Description = FieldDescriptions.JsonPath,
DefaultValue = null DefaultValue = null
} }
}; };

53
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs

@ -9,6 +9,7 @@ using System;
using GraphQL; using GraphQL;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
@ -28,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "id", Name = "id",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id, Resolver = EntityResolvers.Id,
Description = "The id of the asset." Description = FieldDescriptions.EntityId
}); });
AddField(new FieldType AddField(new FieldType
@ -36,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "version", Name = "version",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = EntityResolvers.Version, Resolver = EntityResolvers.Version,
Description = "The version of the asset." Description = FieldDescriptions.EntityVersion
}); });
AddField(new FieldType AddField(new FieldType
@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "created", Name = "created",
ResolvedType = AllTypes.NonNullDateTime, ResolvedType = AllTypes.NonNullDateTime,
Resolver = EntityResolvers.Created, Resolver = EntityResolvers.Created,
Description = "The date and time when the asset has been created." Description = FieldDescriptions.EntityCreated
}); });
AddField(new FieldType AddField(new FieldType
@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "createdBy", Name = "createdBy",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.CreatedBy, Resolver = EntityResolvers.CreatedBy,
Description = "The user that has created the asset." Description = FieldDescriptions.EntityCreatedBy
}); });
AddField(new FieldType AddField(new FieldType
@ -60,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "createdByUser", Name = "createdByUser",
ResolvedType = UserGraphType.NonNull, ResolvedType = UserGraphType.NonNull,
Resolver = EntityResolvers.CreatedByUser, Resolver = EntityResolvers.CreatedByUser,
Description = "The full info of the user that has created the asset." Description = FieldDescriptions.EntityCreatedBy
}); });
AddField(new FieldType AddField(new FieldType
@ -68,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "lastModified", Name = "lastModified",
ResolvedType = AllTypes.NonNullDateTime, ResolvedType = AllTypes.NonNullDateTime,
Resolver = EntityResolvers.LastModified, Resolver = EntityResolvers.LastModified,
Description = "The date and time when the asset has been modified last." Description = FieldDescriptions.EntityLastModified
}); });
AddField(new FieldType AddField(new FieldType
@ -76,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "lastModifiedBy", Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.LastModifiedBy, Resolver = EntityResolvers.LastModifiedBy,
Description = "The user that has updated the asset last." Description = FieldDescriptions.EntityLastModifiedBy
}); });
AddField(new FieldType AddField(new FieldType
@ -84,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "lastModifiedByUser", Name = "lastModifiedByUser",
ResolvedType = UserGraphType.NonNull, ResolvedType = UserGraphType.NonNull,
Resolver = EntityResolvers.LastModifiedByUser, Resolver = EntityResolvers.LastModifiedByUser,
Description = "The full info of the user that has created the asset." Description = FieldDescriptions.EntityLastModifiedBy
}); });
AddField(new FieldType AddField(new FieldType
@ -92,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "mimeType", Name = "mimeType",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.MimeType), Resolver = Resolve(x => x.MimeType),
Description = "The mime type." Description = FieldDescriptions.AssetMimeType
}); });
AddField(new FieldType AddField(new FieldType
@ -100,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "url", Name = "url",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Url, Resolver = Url,
Description = "The url to the asset." Description = FieldDescriptions.AssetUrl
}); });
AddField(new FieldType AddField(new FieldType
@ -108,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "thumbnailUrl", Name = "thumbnailUrl",
ResolvedType = AllTypes.String, ResolvedType = AllTypes.String,
Resolver = ThumbnailUrl, Resolver = ThumbnailUrl,
Description = "The thumbnail url to the asset." Description = FieldDescriptions.AssetThumbnailUrl
}); });
AddField(new FieldType AddField(new FieldType
@ -116,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileName", Name = "fileName",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileName), Resolver = Resolve(x => x.FileName),
Description = "The file name." Description = FieldDescriptions.AssetFileName
}); });
AddField(new FieldType AddField(new FieldType
@ -124,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileHash", Name = "fileHash",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileHash), Resolver = Resolve(x => x.FileHash),
Description = "The hash of the file. Can be null for old files." Description = FieldDescriptions.AssetFileHash
}); });
AddField(new FieldType AddField(new FieldType
@ -132,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileType", Name = "fileType",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileName.FileType()), Resolver = Resolve(x => x.FileName.FileType()),
Description = "The file type." Description = FieldDescriptions.AssetFileType
}); });
AddField(new FieldType AddField(new FieldType
@ -140,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileSize", Name = "fileSize",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = Resolve(x => x.FileSize), Resolver = Resolve(x => x.FileSize),
Description = "The size of the file in bytes." Description = FieldDescriptions.AssetFileSize
}); });
AddField(new FieldType AddField(new FieldType
@ -148,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileVersion", Name = "fileVersion",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = Resolve(x => x.FileVersion), Resolver = Resolve(x => x.FileVersion),
Description = "The version of the file." Description = FieldDescriptions.AssetFileVersion
}); });
AddField(new FieldType AddField(new FieldType
@ -156,7 +157,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "slug", Name = "slug",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Slug), Resolver = Resolve(x => x.Slug),
Description = "The file name as slug." Description = FieldDescriptions.AssetSlug
}); });
AddField(new FieldType AddField(new FieldType
@ -164,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "isProtected", Name = "isProtected",
ResolvedType = AllTypes.NonNullBoolean, ResolvedType = AllTypes.NonNullBoolean,
Resolver = Resolve(x => x.IsProtected), Resolver = Resolve(x => x.IsProtected),
Description = "True, when the asset is not public." Description = FieldDescriptions.AssetIsProtected
}); });
AddField(new FieldType AddField(new FieldType
@ -172,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "isImage", Name = "isImage",
ResolvedType = AllTypes.NonNullBoolean, ResolvedType = AllTypes.NonNullBoolean,
Resolver = Resolve(x => x.Type == AssetType.Image), Resolver = Resolve(x => x.Type == AssetType.Image),
Description = "Determines if the uploaded file is an image.", Description = FieldDescriptions.AssetIsImage,
DeprecationReason = "Use 'type' field instead." DeprecationReason = "Use 'type' field instead."
}); });
@ -181,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "pixelWidth", Name = "pixelWidth",
ResolvedType = AllTypes.Int, ResolvedType = AllTypes.Int,
Resolver = Resolve(x => x.Metadata.GetPixelWidth()), Resolver = Resolve(x => x.Metadata.GetPixelWidth()),
Description = "The width of the image in pixels if the asset is an image.", Description = FieldDescriptions.AssetPixelWidth,
DeprecationReason = "Use 'metadata' field instead." DeprecationReason = "Use 'metadata' field instead."
}); });
@ -190,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "pixelHeight", Name = "pixelHeight",
ResolvedType = AllTypes.Int, ResolvedType = AllTypes.Int,
Resolver = Resolve(x => x.Metadata.GetPixelHeight()), Resolver = Resolve(x => x.Metadata.GetPixelHeight()),
Description = "The height of the image in pixels if the asset is an image.", Description = FieldDescriptions.AssetPixelHeight,
DeprecationReason = "Use 'metadata' field instead." DeprecationReason = "Use 'metadata' field instead."
}); });
@ -199,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "type", Name = "type",
ResolvedType = AllTypes.NonNullAssetType, ResolvedType = AllTypes.NonNullAssetType,
Resolver = Resolve(x => x.Type), Resolver = Resolve(x => x.Type),
Description = "The type of the image." Description = FieldDescriptions.AssetType
}); });
AddField(new FieldType AddField(new FieldType
@ -207,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "metadataText", Name = "metadataText",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.MetadataText), Resolver = Resolve(x => x.MetadataText),
Description = "The text representation of the metadata." Description = FieldDescriptions.AssetMetadataText
}); });
AddField(new FieldType AddField(new FieldType
@ -215,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "tags", Name = "tags",
ResolvedType = AllTypes.NonNullStrings, ResolvedType = AllTypes.NonNullStrings,
Resolver = Resolve(x => x.TagNames), Resolver = Resolve(x => x.TagNames),
Description = "The asset tags." Description = FieldDescriptions.AssetTags
}); });
AddField(new FieldType AddField(new FieldType
@ -224,7 +225,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Arguments = AssetActions.Metadata.Arguments, Arguments = AssetActions.Metadata.Arguments,
ResolvedType = AllTypes.JsonNoop, ResolvedType = AllTypes.JsonNoop,
Resolver = AssetActions.Metadata.Resolver, Resolver = AssetActions.Metadata.Resolver,
Description = "The asset metadata." Description = FieldDescriptions.AssetMetadata
}); });
if (canGenerateSourceUrl) if (canGenerateSourceUrl)
@ -234,7 +235,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "sourceUrl", Name = "sourceUrl",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = SourceUrl, Resolver = SourceUrl,
Description = "The source url of the asset." Description = FieldDescriptions.AssetSourceUrl
}); });
} }

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs

@ -8,6 +8,7 @@
using System; using System;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -25,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "total", Name = "total",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = ResolveList(x => x.Total), Resolver = ResolveList(x => x.Total),
Description = "The total count of assets." Description = FieldDescriptions.AssetsTotal
}); });
AddField(new FieldType AddField(new FieldType
@ -33,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "items", Name = "items",
ResolvedType = new NonNullGraphType(assetsList), ResolvedType = new NonNullGraphType(assetsList),
Resolver = ResolveList(x => x), Resolver = ResolveList(x => x),
Description = "The assets." Description = FieldDescriptions.AssetsItems
}); });
Description = "List of assets and total count of assets."; Description = "List of assets and total count of assets.";

59
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs

@ -10,6 +10,7 @@ using GraphQL;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -28,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "path", Name = "path",
Description = "The path to the json value", Description = FieldDescriptions.JsonPath,
DefaultValue = null DefaultValue = null
} }
}; };
@ -51,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "path", Name = "path",
Description = "The path to the json value", Description = FieldDescriptions.JsonPath,
DefaultValue = null DefaultValue = null
} }
}; };
@ -63,13 +64,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.NonNullString) new QueryArgument(AllTypes.NonNullString)
{ {
Name = "id", Name = "id",
Description = "The id of the content (usually GUID).", Description = FieldDescriptions.EntityId,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "version", Name = "version",
Description = "The optional version of the content to retrieve an older instance (not cached).", Description = FieldDescriptions.QueryVersion,
DefaultValue = null DefaultValue = null
} }
}; };
@ -98,31 +99,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "top", Name = "top",
Description = "Optional number of contents to take.", Description = FieldDescriptions.QueryTop,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "skip", Name = "skip",
Description = "Optional number of contents to skip.", Description = FieldDescriptions.QuerySkip,
DefaultValue = 0 DefaultValue = 0
}, },
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "filter", Name = "filter",
Description = "Optional OData filter.", Description = FieldDescriptions.QueryFilter,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "orderby", Name = "orderby",
Description = "Optional OData order definition.", Description = FieldDescriptions.QueryOrderBy,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "search", Name = "search",
Description = "Optional OData full text search.", Description = FieldDescriptions.QuerySearch,
DefaultValue = null DefaultValue = null
} }
}; };
@ -173,25 +174,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(new NonNullGraphType(inputType)) new QueryArgument(new NonNullGraphType(inputType))
{ {
Name = "data", Name = "data",
Description = "The data for the content.", Description = FieldDescriptions.ContentRequestData,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Boolean) new QueryArgument(AllTypes.Boolean)
{ {
Name = "publish", Name = "publish",
Description = "Set to true to autopublish content on create.", Description = FieldDescriptions.ContentRequestPublish,
DefaultValue = false DefaultValue = false
}, },
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "status", Name = "status",
Description = "The initial status.", Description = FieldDescriptions.ContentRequestOptionalStatus,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "id", Name = "id",
Description = "The optional custom content id.", Description = FieldDescriptions.ContentRequestOptionalId,
DefaultValue = null DefaultValue = null
} }
}; };
@ -232,31 +233,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.NonNullString) new QueryArgument(AllTypes.NonNullString)
{ {
Name = "id", Name = "id",
Description = "The id of the content (usually GUID).", Description = FieldDescriptions.EntityId,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(new NonNullGraphType(inputType)) new QueryArgument(new NonNullGraphType(inputType))
{ {
Name = "data", Name = "data",
Description = "The data for the content.", Description = FieldDescriptions.ContentRequestData,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Boolean) new QueryArgument(AllTypes.Boolean)
{ {
Name = "publish", Name = "publish",
Description = "Set to true to autopublish content on create.", Description = FieldDescriptions.ContentRequestPublish,
DefaultValue = false DefaultValue = false
}, },
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "status", Name = "status",
Description = "The initial status.", Description = FieldDescriptions.ContentRequestOptionalStatus,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "expectedVersion", Name = "expectedVersion",
Description = "The expected version", Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any DefaultValue = EtagVersion.Any
} }
}; };
@ -294,19 +295,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "id", Name = "id",
Description = "The optional custom content id.", Description = FieldDescriptions.EntityId,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(new NonNullGraphType(inputType)) new QueryArgument(new NonNullGraphType(inputType))
{ {
Name = "data", Name = "data",
Description = "The data for the content.", Description = FieldDescriptions.ContentRequestData,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "expectedVersion", Name = "expectedVersion",
Description = "The expected version", Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any DefaultValue = EtagVersion.Any
} }
}; };
@ -330,19 +331,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String) new QueryArgument(AllTypes.String)
{ {
Name = "id", Name = "id",
Description = "The optional custom content id.", Description = FieldDescriptions.EntityId,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(new NonNullGraphType(inputType)) new QueryArgument(new NonNullGraphType(inputType))
{ {
Name = "data", Name = "data",
Description = "The data for the content.", Description = FieldDescriptions.ContentRequestData,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "expectedVersion", Name = "expectedVersion",
Description = "The expected version", Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any DefaultValue = EtagVersion.Any
} }
}; };
@ -364,25 +365,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.NonNullString) new QueryArgument(AllTypes.NonNullString)
{ {
Name = "id", Name = "id",
Description = "The id of the content (usually GUID).", Description = FieldDescriptions.EntityId,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.NonNullString) new QueryArgument(AllTypes.NonNullString)
{ {
Name = "status", Name = "status",
Description = "The new status", Description = FieldDescriptions.ContentRequestStatus,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.DateTime) new QueryArgument(AllTypes.DateTime)
{ {
Name = "dueTime", Name = "dueTime",
Description = "When to change the status", Description = FieldDescriptions.ContentRequestDueTime,
DefaultValue = null DefaultValue = null
}, },
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "expectedVersion", Name = "expectedVersion",
Description = "The expected version", Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any DefaultValue = EtagVersion.Any
} }
}; };
@ -410,7 +411,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.Int) new QueryArgument(AllTypes.Int)
{ {
Name = "expectedVersion", Name = "expectedVersion",
Description = "The expected version", Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any DefaultValue = EtagVersion.Any
} }
}; };

29
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs

@ -8,6 +8,7 @@
using System; using System;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -21,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "id", Name = "id",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id, Resolver = EntityResolvers.Id,
Description = "The id of the content." Description = FieldDescriptions.EntityId
}; };
public static readonly FieldType Version = new FieldType public static readonly FieldType Version = new FieldType
@ -29,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "version", Name = "version",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = EntityResolvers.Version, Resolver = EntityResolvers.Version,
Description = "The version of the content." Description = FieldDescriptions.EntityVersion
}; };
public static readonly FieldType Created = new FieldType public static readonly FieldType Created = new FieldType
@ -37,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "created", Name = "created",
ResolvedType = AllTypes.NonNullDateTime, ResolvedType = AllTypes.NonNullDateTime,
Resolver = EntityResolvers.Created, Resolver = EntityResolvers.Created,
Description = "The date and time when the content has been created." Description = FieldDescriptions.EntityCreated
}; };
public static readonly FieldType CreatedBy = new FieldType public static readonly FieldType CreatedBy = new FieldType
@ -45,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "createdBy", Name = "createdBy",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.CreatedBy, Resolver = EntityResolvers.CreatedBy,
Description = "The user that has created the content." Description = FieldDescriptions.EntityCreatedBy
}; };
public static readonly FieldType CreatedByUser = new FieldType public static readonly FieldType CreatedByUser = new FieldType
@ -53,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "createdByUser", Name = "createdByUser",
ResolvedType = UserGraphType.NonNull, ResolvedType = UserGraphType.NonNull,
Resolver = EntityResolvers.CreatedByUser, Resolver = EntityResolvers.CreatedByUser,
Description = "The full info of the user that has created the content." Description = FieldDescriptions.EntityCreatedBy
}; };
public static readonly FieldType LastModified = new FieldType public static readonly FieldType LastModified = new FieldType
@ -61,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "lastModified", Name = "lastModified",
ResolvedType = AllTypes.NonNullDateTime, ResolvedType = AllTypes.NonNullDateTime,
Resolver = EntityResolvers.LastModified, Resolver = EntityResolvers.LastModified,
Description = "The date and time when the content has been modified last." Description = FieldDescriptions.EntityLastModified
}; };
public static readonly FieldType LastModifiedBy = new FieldType public static readonly FieldType LastModifiedBy = new FieldType
@ -69,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "lastModifiedBy", Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.LastModifiedBy, Resolver = EntityResolvers.LastModifiedBy,
Description = "The user that has updated the content last." Description = FieldDescriptions.EntityLastModifiedBy
}; };
public static readonly FieldType LastModifiedByUser = new FieldType public static readonly FieldType LastModifiedByUser = new FieldType
@ -77,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "lastModifiedByUser", Name = "lastModifiedByUser",
ResolvedType = UserGraphType.NonNull, ResolvedType = UserGraphType.NonNull,
Resolver = EntityResolvers.LastModifiedByUser, Resolver = EntityResolvers.LastModifiedByUser,
Description = "The full info of the user that has updated the content last." Description = FieldDescriptions.EntityLastModifiedBy
}; };
public static readonly FieldType Status = new FieldType public static readonly FieldType Status = new FieldType
@ -85,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "status", Name = "status",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Status.ToString().ToUpperInvariant()), Resolver = Resolve(x => x.Status.ToString().ToUpperInvariant()),
Description = "The status of the content." Description = FieldDescriptions.ContentStatus
}; };
public static readonly FieldType StatusColor = new FieldType public static readonly FieldType StatusColor = new FieldType
@ -93,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "statusColor", Name = "statusColor",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.StatusColor), Resolver = Resolve(x => x.StatusColor),
Description = "The status color of the content." Description = FieldDescriptions.ContentStatusColor
}; };
public static readonly FieldType NewStatus = new FieldType public static readonly FieldType NewStatus = new FieldType
@ -101,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "newStatus", Name = "newStatus",
ResolvedType = AllTypes.String, ResolvedType = AllTypes.String,
Resolver = Resolve(x => x.NewStatus?.ToString().ToUpperInvariant()), Resolver = Resolve(x => x.NewStatus?.ToString().ToUpperInvariant()),
Description = "The new status of the content." Description = FieldDescriptions.ContentNewStatus
}; };
public static readonly FieldType NewStatusColor = new FieldType public static readonly FieldType NewStatusColor = new FieldType
@ -109,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "newStatusColor", Name = "newStatusColor",
ResolvedType = AllTypes.String, ResolvedType = AllTypes.String,
Resolver = Resolve(x => x.NewStatusColor), Resolver = Resolve(x => x.NewStatusColor),
Description = "The new status color of the content." Description = FieldDescriptions.ContentStatusColor
}; };
public static readonly FieldType SchemaId = new FieldType public static readonly FieldType SchemaId = new FieldType
@ -117,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "schemaId", Name = "schemaId",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x[Component.Discriminator].ToString()), Resolver = Resolve(x => x[Component.Discriminator].ToString()),
Description = "The id of the schema." Description = FieldDescriptions.ContentSchemaId
}; };
public static readonly FieldType Url = new FieldType public static readonly FieldType Url = new FieldType
@ -125,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "url", Name = "url",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.Url, Resolver = ContentResolvers.Url,
Description = "The url to the content." Description = FieldDescriptions.ContentUrl
}; };
private static IFieldResolver Resolve<T>(Func<JsonObject, T> resolver) private static IFieldResolver Resolve<T>(Func<JsonObject, T> resolver)

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs

@ -8,6 +8,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -57,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "data", Name = "data",
ResolvedType = new NonNullGraphType(contentDataType), ResolvedType = new NonNullGraphType(contentDataType),
Resolver = ContentResolvers.Data, Resolver = ContentResolvers.Data,
Description = "The data of the content." Description = FieldDescriptions.ContentData
}); });
} }
@ -70,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "flatData", Name = "flatData",
ResolvedType = new NonNullGraphType(contentDataTypeFlat), ResolvedType = new NonNullGraphType(contentDataTypeFlat),
Resolver = ContentResolvers.FlatData, Resolver = ContentResolvers.FlatData,
Description = "The flat data of the content." Description = FieldDescriptions.ContentFlatData
}); });
} }

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResultGraphType.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
@ -22,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "total", Name = "total",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = ContentResolvers.ListTotal, Resolver = ContentResolvers.ListTotal,
Description = $"The total number of {schemaInfo.DisplayName} items." Description = FieldDescriptions.ContentsTotal
}); });
AddField(new FieldType AddField(new FieldType
@ -30,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "items", Name = "items",
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = ContentResolvers.ListItems, Resolver = ContentResolvers.ListItems,
Description = $"The {schemaInfo.DisplayName} items." Description = FieldDescriptions.ContentsItems
}); });
Description = $"List of {schemaInfo.DisplayName} items and total count."; Description = $"List of {schemaInfo.DisplayName} items and total count.";

7
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/UserGraphType.cs

@ -8,6 +8,7 @@
using System; using System;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users; using Squidex.Shared.Users;
@ -29,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "id", Name = "id",
Resolver = Resolve(x => x.Id), Resolver = Resolve(x => x.Id),
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Description = "The id of the user." Description = FieldDescriptions.UserId
}); });
AddField(new FieldType AddField(new FieldType
@ -37,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "displayName", Name = "displayName",
Resolver = Resolve(x => x.Claims.DisplayName()), Resolver = Resolve(x => x.Claims.DisplayName()),
ResolvedType = AllTypes.String, ResolvedType = AllTypes.String,
Description = "The display name of the user." Description = FieldDescriptions.UserDisplayName
}); });
AddField(new FieldType AddField(new FieldType
@ -45,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "email", Name = "email",
Resolver = Resolve(x => x.Email), Resolver = Resolve(x => x.Email),
ResolvedType = AllTypes.String, ResolvedType = AllTypes.String,
Description = "The email of the user." Description = FieldDescriptions.UserEmail
}); });
Description = "A user that created or modified a content or asset."; Description = "A user that created or modified a content or asset.";

18
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null); private readonly EdmModel genericEdmModel = BuildEdmModel("Generic", "Content", new EdmModel(), null);
private readonly JsonSchema genericJsonSchema = ContentJsonSchemaBuilder.BuildSchema("Content", null, false, true); private readonly JsonSchema genericJsonSchema = ContentJsonSchemaBuilder.BuildSchema(null, false, true);
private readonly IMemoryCache cache; private readonly IMemoryCache cache;
private readonly IJsonSerializer jsonSerializer; private readonly IJsonSerializer jsonSerializer;
private readonly IAppProvider appprovider; private readonly IAppProvider appprovider;
@ -232,6 +232,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return result; return result;
} }
private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app,
ResolvedComponents components, bool withHiddenFields)
{
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), components, withHiddenFields);
return ContentJsonSchemaBuilder.BuildSchema(dataSchema, false, true);
}
private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema, private IEdmModel BuildEdmModel(Context context, ISchemaEntity? schema,
ResolvedComponents components) ResolvedComponents components)
{ {
@ -252,14 +260,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return result; return result;
} }
private static JsonSchema BuildJsonSchema(Schema schema, IAppEntity app,
ResolvedComponents components, bool withHiddenFields)
{
var dataSchema = schema.BuildJsonSchema(app.PartitionResolver(), components, withHiddenFields);
return ContentJsonSchemaBuilder.BuildSchema(schema.DisplayName(), dataSchema, false, true);
}
private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, private static EdmModel BuildEdmModel(Schema schema, IAppEntity app,
ResolvedComponents components, bool withHiddenFields) ResolvedComponents components, bool withHiddenFields)
{ {

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

@ -17,6 +17,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
private readonly IScriptEngine scriptEngine; 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) public ScriptContent(IScriptEngine scriptEngine)
{ {
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
@ -46,11 +56,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
var vars = new ScriptVars var vars = new ScriptVars
{ {
ContentId = content.Id, [ScriptKeys.AppId] = context.App.Id,
Data = content.Data, [ScriptKeys.AppName] = context.App.Name,
AppId = context.App.Id, [ScriptKeys.ContentId] = content.Id,
AppName = context.App.Name, [ScriptKeys.Data] = content.Data,
User = context.User [ScriptKeys.User] = context.User
}; };
var options = new ScriptOptions var options = new ScriptOptions

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

@ -32,12 +32,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
public void ExtendAsync(ExecutionContext context) public void ExtendAsync(ExecutionContext context)
{ {
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId)) if (!context.TryGetValue<DomainId>("appId", out var appId))
{ {
return; return;
} }
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user)) if (!context.TryGetValue<ClaimsPrincipal>("user", out var user))
{ {
return; return;
} }

28
backend/src/Squidex.Domain.Apps.Entities/Contents/Schemas/ContentJsonSchemaBuilder.cs

@ -11,39 +11,39 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema
{ {
public static class ContentJsonSchemaBuilder public static class ContentJsonSchemaBuilder
{ {
public static JsonSchema BuildSchema(string name, JsonSchema? dataSchema, bool extended = false, bool withDeleted = false) public static JsonSchema BuildSchema(JsonSchema? dataSchema, bool extended = false, bool withDeleted = false)
{ {
var jsonSchema = new JsonSchema var jsonSchema = new JsonSchema
{ {
Properties = Properties =
{ {
["id"] = SchemaBuilder.StringProperty($"The id of the {name} content.", true), ["id"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityId, true),
["created"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been created.", true), ["created"] = SchemaBuilder.DateTimeProperty(FieldDescriptions.EntityCreated, true),
["createdBy"] = SchemaBuilder.StringProperty($"The user that has created the {name} content.", true), ["createdBy"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityCreatedBy, true),
["lastModified"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been modified last.", true), ["lastModified"] = SchemaBuilder.DateTimeProperty(FieldDescriptions.EntityLastModified, true),
["lastModifiedBy"] = SchemaBuilder.StringProperty($"The user that has updated the {name} content last.", true), ["lastModifiedBy"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityLastModifiedBy, true),
["newStatus"] = SchemaBuilder.StringProperty("The new status of the content."), ["newStatus"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentNewStatus),
["status"] = SchemaBuilder.StringProperty("The status of the content.", true), ["status"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentStatus, true)
}, },
Type = JsonObjectType.Object Type = JsonObjectType.Object
}; };
if (withDeleted) if (withDeleted)
{ {
jsonSchema.Properties["isDeleted"] = SchemaBuilder.BooleanProperty("True when deleted.", false); jsonSchema.Properties["isDeleted"] = SchemaBuilder.BooleanProperty(FieldDescriptions.EntityIsDeleted, false);
} }
if (extended) if (extended)
{ {
jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty("The color of the new status.", false); jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentNewStatusColor, false);
jsonSchema.Properties["schema"] = SchemaBuilder.StringProperty("The name of the schema.", true); jsonSchema.Properties["schema"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentSchema, true);
jsonSchema.Properties["SchemaName"] = SchemaBuilder.StringProperty("The display name of the schema.", true); jsonSchema.Properties["SchemaName"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentSchemaName, true);
jsonSchema.Properties["statusColor"] = SchemaBuilder.StringProperty("The color of the status.", true); jsonSchema.Properties["statusColor"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentStatusColor, true);
} }
if (dataSchema != null) if (dataSchema != null)
{ {
jsonSchema.Properties["data"] = SchemaBuilder.ReferenceProperty(dataSchema, $"The data of the {name}.", true); jsonSchema.Properties["data"] = SchemaBuilder.ReferenceProperty(dataSchema, FieldDescriptions.ContentData, true);
} }
return jsonSchema; return jsonSchema;

85
backend/src/Squidex.Domain.Apps.Entities/OperationContextBase.cs

@ -0,0 +1,85 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities
{
public abstract class OperationContextBase<TCommand, TSnapShot> where TCommand : SquidexCommand, IAggregateCommand
{
private readonly List<ValidationError> errors = new List<ValidationError>();
private readonly IServiceProvider serviceProvider;
private readonly Func<TSnapShot> snapshot;
public RefToken Actor => Command.Actor;
public IAppEntity App { get; init; }
public DomainId CommandId { get; init; }
public TCommand Command { get; init; }
public TSnapShot Snapshot => snapshot();
public ClaimsPrincipal? User => Command.User;
protected OperationContextBase(IServiceProvider serviceProvider, Func<TSnapShot> snapshot)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(snapshot, nameof(snapshot));
this.serviceProvider = serviceProvider;
this.snapshot = snapshot;
}
public T Resolve<T>() where T : notnull
{
return serviceProvider.GetRequiredService<T>();
}
public T? ResolveOptional<T>() where T : class
{
return serviceProvider.GetService(typeof(T)) as T;
}
public OperationContextBase<TCommand, TSnapShot> AddError(string message, params string[] propertyNames)
{
errors.Add(new ValidationError(message, propertyNames));
return this;
}
public OperationContextBase<TCommand, TSnapShot> AddError(ValidationError newError)
{
errors.Add(newError);
return this;
}
public OperationContextBase<TCommand, TSnapShot> AddErrors(IEnumerable<ValidationError> newErrors)
{
errors.AddRange(newErrors);
return this;
}
public void ThrowOnErrors()
{
if (errors.Count > 0)
{
throw new ValidationException(errors);
}
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
var ruleContext = new RuleContext var ruleContext = new RuleContext
{ {
Rule = rule, Rule = rule,
RuleId = ruleId, RuleId = ruleId
}; };
var jobs = ruleService.CreateJobsAsync(@event, ruleContext); var jobs = ruleService.CreateJobsAsync(@event, ruleContext);

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ChangeCategory.cs

@ -9,6 +9,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
public sealed class ChangeCategory : SchemaUpdateCommand public sealed class ChangeCategory : SchemaUpdateCommand
{ {
public string Name { get; set; } public string? Name { get; set; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigurePreviewUrls.cs

@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
public sealed class ConfigurePreviewUrls : SchemaUpdateCommand public sealed class ConfigurePreviewUrls : SchemaUpdateCommand
{ {
public ImmutableDictionary<string, string> PreviewUrls { get; set; } public ImmutableDictionary<string, string>? PreviewUrls { get; set; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureScripts.cs

@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands
{ {
public sealed class ConfigureScripts : SchemaUpdateCommand public sealed class ConfigureScripts : SchemaUpdateCommand
{ {
public SchemaScripts Scripts { get; set; } public SchemaScripts? Scripts { get; set; }
} }
} }

20
backend/src/Squidex.Domain.Apps.Entities/Scripting/JsonType.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.Entities.Scripting
{
public enum JsonType
{
Any,
Array,
Boolean,
Function,
Number,
Object,
String
}
}

316
backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingCompletion.cs

@ -0,0 +1,316 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Scripting
{
public sealed class ScriptingCompletion
{
private readonly Stack<string> prefixes = new Stack<string>();
private readonly HashSet<ScriptingValue> result = new HashSet<ScriptingValue>();
public IReadOnlyList<ScriptingValue> Content(Schema schema, PartitionResolver partitionResolver)
{
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddFunction("replace()",
"Tell Squidex that you have modified the data and that the change should be applied.");
AddFunction("getReferences(ids, callback)",
"Queries the content items with the specified IDs and invokes the callback with an array of contents.");
AddFunction("getReference(ids, callback)",
"Queries the content item with the specified ID and invokes the callback with an array of contents.");
AddFunction("getAssets(ids, callback)",
"Queries the assets with the specified IDs and invokes the callback with an array of assets.");
AddFunction("getAsset(ids, callback)",
"Queries the asset with the specified ID and invokes the callback with an array of assets.");
AddShared();
AddString("contentId",
FieldDescriptions.EntityId);
AddString("status",
FieldDescriptions.ContentStatus);
AddString("statusOld",
FieldDescriptions.ContentStatusOld);
AddObject("data", FieldDescriptions.ContentData, () =>
{
AddData(schema, partitionResolver);
});
AddObject("dataOld", FieldDescriptions.ContentDataOld, () =>
{
AddData(schema, partitionResolver);
});
});
return result.OrderBy(x => x.Path).ToList();
}
public IReadOnlyList<ScriptingValue> Asset()
{
AddObject("ctx", FieldDescriptions.Context, () =>
{
AddShared();
AddString("assetId",
FieldDescriptions.EntityId);
AddObject("asset",
FieldDescriptions.Asset, () =>
{
AddSharedAsset();
AddNumber("fileVersion",
FieldDescriptions.AssetFileVersion);
});
AddObject("command",
FieldDescriptions.Command, () =>
{
AddSharedAsset();
AddBoolean("permanent",
FieldDescriptions.EntityRequestDeletePermanent);
});
});
return result.OrderBy(x => x.Path).ToList();
}
private void AddSharedAsset()
{
AddString("fileHash",
FieldDescriptions.AssetFileHash);
AddString("fileName",
FieldDescriptions.AssetFileName);
AddString("fileSize",
FieldDescriptions.AssetFileSize);
AddString("fileSlug",
FieldDescriptions.AssetSlug);
AddString("mimeType",
FieldDescriptions.AssetMimeType);
AddBoolean("isProtected",
FieldDescriptions.AssetIsProtected);
AddString("parentId",
FieldDescriptions.AssetParentId);
AddArray("parentPath",
FieldDescriptions.AssetParentPath);
AddArray("tags",
FieldDescriptions.AssetTags);
AddObject("metadata",
FieldDescriptions.AssetMetadata, () =>
{
AddArray("name",
FieldDescriptions.AssetMetadataValue);
});
}
private void AddShared()
{
AddFunction("disallow()",
"Tell Squidex to not allow the current operation and to return a 403 (Forbidden).");
AddFunction("reject('Reason')",
"Tell Squidex to reject the current operation and to return a 403 (Forbidden).");
AddFunction("html2Text(text)",
"Converts a HTML string to plain text.");
AddFunction("markdown2Text(text)",
"Converts a markdown string to plain text.");
AddFunction("formatDate(data, pattern)",
"Formats a JavaScript date object using the specified pattern.");
AddFunction("formatTime(text)",
"Formats a JavaScript date object using the specified pattern.");
AddFunction("wordCount(text)",
"Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.");
AddFunction("characterCount(text)",
"Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text.");
AddFunction("toCamelCase(text)",
"Converts a text to camelCase.");
AddFunction("toPascalCase(text)",
"Calculate the SHA256 hash from a given string. Use this method for hashing passwords");
AddFunction("sha256(text)",
"Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("slugify(text)",
"Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.");
AddFunction("getJSON(url, callback, ?headers)",
"Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("postJSON(url, body, callback, ?headers)",
"Makes a POST request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("putJSON(url, body, callback, ?headers)",
"Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("putJSON(url, body, callback, ?headers)",
"Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("patchJSON(url, body, callback, headers)",
"Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional.");
AddFunction("deleteJSON(url, body, callback, headers)",
"Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.");
AddString("appId",
FieldDescriptions.AppId);
AddString("appName",
FieldDescriptions.AppName);
AddString("operation",
FieldDescriptions.Operation);
AddObject("user",
FieldDescriptions.User, () =>
{
AddString("id",
FieldDescriptions.UserId);
AddString("email",
FieldDescriptions.UserEmail);
AddBoolean("isClient",
FieldDescriptions.UserIsClient);
AddBoolean("isUser",
FieldDescriptions.UserIsUser);
AddObject("claims",
FieldDescriptions.UserClaims, () =>
{
AddArray("name",
FieldDescriptions.UsersClaimsValue);
});
});
}
private void AddData(Schema schema, PartitionResolver partitionResolver)
{
foreach (var field in schema.Fields.Where(x => x.IsForApi(true)))
{
var description = $"The values of the '{field.DisplayName()}' field.";
AddObject(field.Name, $"The values of the '{field.DisplayName()}' field.", () =>
{
foreach (var partition in partitionResolver(field.Partitioning).AllKeys)
{
var description = $"The '{partition}' value of the '{field.DisplayName()}' field.";
if (field is ArrayField arrayField)
{
AddObject(partition, description, () =>
{
foreach (var nestedField in arrayField.Fields.Where(x => x.IsForApi(true)))
{
var description = $"The value of the '{nestedField.DisplayName()}' nested field.";
AddAny(field.Name, description);
}
});
}
else
{
AddAny(partition, description);
}
}
});
}
}
private void AddAny(string name, string description)
{
Add(JsonType.Any, name, description);
}
private void AddArray(string name, string description)
{
Add(JsonType.Array, name, description);
}
private void AddBoolean(string name, string description)
{
Add(JsonType.Boolean, name, description);
}
private void AddFunction(string name, string description)
{
Add(JsonType.Function, name, description);
}
private void AddNumber(string name, string description)
{
Add(JsonType.Number, name, description);
}
private void AddString(string name, string description)
{
Add(JsonType.String, name, description);
}
private void Add(JsonType type, string name, string description)
{
var fullName = string.Join('.', prefixes.Reverse().Union(Enumerable.Repeat(name, 1)));
result.Add(new ScriptingValue(fullName, type, description));
}
private void AddObject(string name, string description, Action inner)
{
Add(JsonType.Object, description, name);
prefixes.Push(name);
try
{
inner();
}
finally
{
prefixes.Pop();
}
}
}
}

15
backend/src/Squidex.Domain.Apps.Entities/Scripting/ScriptingValue.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Entities.Scripting
{
public sealed record ScriptingValue(string Path, JsonType Type, string Description)
{
}
}

18
backend/src/Squidex.Domain.Apps.Events/Apps/AppAssetsScriptsConfigured.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppAssetsScriptsConfigured))]
public sealed class AppAssetsScriptsConfigured : AppEvent
{
public AssetScripts? Scripts { get; set; }
}
}

2
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaCategoryChanged.cs

@ -12,6 +12,6 @@ namespace Squidex.Domain.Apps.Events.Schemas
[EventType(nameof(SchemaCategoryChanged))] [EventType(nameof(SchemaCategoryChanged))]
public sealed class SchemaCategoryChanged : SchemaEvent public sealed class SchemaCategoryChanged : SchemaEvent
{ {
public string Name { get; set; } public string? Name { get; set; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs

@ -13,6 +13,6 @@ namespace Squidex.Domain.Apps.Events.Schemas
[EventType(nameof(SchemaFieldRulesConfigured))] [EventType(nameof(SchemaFieldRulesConfigured))]
public sealed class SchemaFieldRulesConfigured : SchemaEvent public sealed class SchemaFieldRulesConfigured : SchemaEvent
{ {
public FieldRules FieldRules { get; set; } public FieldRules? FieldRules { get; set; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaPreviewUrlsConfigured.cs

@ -13,6 +13,6 @@ namespace Squidex.Domain.Apps.Events.Schemas
[EventType(nameof(SchemaPreviewUrlsConfigured))] [EventType(nameof(SchemaPreviewUrlsConfigured))]
public sealed class SchemaPreviewUrlsConfigured : SchemaEvent public sealed class SchemaPreviewUrlsConfigured : SchemaEvent
{ {
public ImmutableDictionary<string, string> PreviewUrls { get; set; } public ImmutableDictionary<string, string>? PreviewUrls { get; set; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaScriptsConfigured.cs

@ -13,6 +13,6 @@ namespace Squidex.Domain.Apps.Events.Schemas
[EventType(nameof(SchemaScriptsConfigured))] [EventType(nameof(SchemaScriptsConfigured))]
public sealed class SchemaScriptsConfigured : SchemaEvent public sealed class SchemaScriptsConfigured : SchemaEvent
{ {
public SchemaScripts Scripts { get; set; } public SchemaScripts? Scripts { get; set; }
} }
} }

4
backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaUIFieldsConfigured.cs

@ -13,8 +13,8 @@ namespace Squidex.Domain.Apps.Events.Schemas
[EventType(nameof(SchemaUIFieldsConfigured))] [EventType(nameof(SchemaUIFieldsConfigured))]
public sealed class SchemaUIFieldsConfigured : SchemaEvent public sealed class SchemaUIFieldsConfigured : SchemaEvent
{ {
public FieldNames FieldsInLists { get; set; } public FieldNames? FieldsInLists { get; set; }
public FieldNames FieldsInReferences { get; set; } public FieldNames? FieldsInReferences { get; set; }
} }
} }

12
backend/src/Squidex.Infrastructure/Json/Objects/JsonArray.cs

@ -35,21 +35,11 @@ namespace Squidex.Infrastructure.Json.Objects
{ {
} }
private JsonArray(List<IJsonValue> source) public JsonArray(List<IJsonValue> source)
: base(source) : base(source)
{ {
} }
internal JsonArray(IEnumerable<object?>? values)
: base(ToList(values))
{
}
private static List<IJsonValue> ToList(IEnumerable<object?>? values)
{
return values?.Select(JsonValue.Create).ToList() ?? new List<IJsonValue>();
}
protected override void InsertItem(int index, IJsonValue item) protected override void InsertItem(int index, IJsonValue item)
{ {
base.InsertItem(index, item ?? JsonValue.Null); base.InsertItem(index, item ?? JsonValue.Null);

6
backend/src/Squidex.Infrastructure/Json/Objects/JsonObject.cs

@ -63,11 +63,15 @@ namespace Squidex.Infrastructure.Json.Objects
public JsonObject(JsonObject obj) public JsonObject(JsonObject obj)
{ {
Guard.NotNull(obj, nameof(obj));
inner = new Dictionary<string, IJsonValue>(obj.inner); inner = new Dictionary<string, IJsonValue>(obj.inner);
} }
private JsonObject(Dictionary<string, IJsonValue> source) public JsonObject(Dictionary<string, IJsonValue> source)
{ {
Guard.NotNull(source, nameof(source));
inner = source; inner = source;
} }

19
backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs

@ -35,12 +35,16 @@ namespace Squidex.Infrastructure.Json.Objects
public static JsonArray Array<T>(IEnumerable<T> values) public static JsonArray Array<T>(IEnumerable<T> values)
{ {
return new JsonArray(values?.OfType<object>()); var source = values?.OfType<object?>().Select(Create).ToList() ?? new List<IJsonValue>();
return new JsonArray(source);
} }
public static JsonArray Array<T>(params T?[] values) public static JsonArray Array<T>(params T?[] values)
{ {
return new JsonArray(values?.OfType<object>()); var source = values?.OfType<object?>().Select(Create).ToList() ?? new List<IJsonValue>();
return new JsonArray(source);
} }
public static JsonObject Object() public static JsonObject Object()
@ -48,6 +52,13 @@ namespace Squidex.Infrastructure.Json.Objects
return new JsonObject(); return new JsonObject();
} }
public static JsonObject Object<T>(IReadOnlyDictionary<string, T> values)
{
var source = values?.ToDictionary(x => x.Key, x => Create(x.Value)) ?? new Dictionary<string, IJsonValue>();
return new JsonObject(source);
}
public static IJsonValue Create(object? value) public static IJsonValue Create(object? value)
{ {
if (value == null) if (value == null)
@ -80,6 +91,10 @@ namespace Squidex.Infrastructure.Json.Objects
return Create(i); return Create(i);
case Instant i: case Instant i:
return Create(i); return Create(i);
case object[] array:
return Array(array);
case IReadOnlyDictionary<string, object?> obj:
return Object(obj);
} }
throw new ArgumentException("Invalid json type"); throw new ArgumentException("Invalid json type");

2
backend/src/Squidex.Infrastructure/Queries/Json/OperatorValidator.cs

@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.Queries.Json
private static readonly CompareOperator[] GeoOperators = private static readonly CompareOperator[] GeoOperators =
{ {
CompareOperator.LessThan, CompareOperator.LessThan,
CompareOperator.Exists, CompareOperator.Exists
}; };
public static bool IsAllowedOperator(JsonSchema schema, CompareOperator compareOperator) public static bool IsAllowedOperator(JsonSchema schema, CompareOperator compareOperator)

4
backend/src/Squidex.Shared/Permissions.cs

@ -125,6 +125,10 @@ namespace Squidex.Shared
public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update"; public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update";
public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete"; public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete";
public const string AppAssetScripts = "squidex.apps.{app}.asset-scripts";
public const string AppAssetSScriptsRead = "squidex.apps.{app}.asset-scripts.read";
public const string AppAssetsScriptsUpdate = "squidex.apps.{app}.asset-scripts.update";
// Rules // Rules
public const string AppRules = "squidex.apps.{app}.rules"; public const string AppRules = "squidex.apps.{app}.rules";
public const string AppRulesRead = "squidex.apps.{app}.rules.read"; public const string AppRulesRead = "squidex.apps.{app}.rules.read";

4
backend/src/Squidex.Web/Resources.cs

@ -51,6 +51,10 @@ namespace Squidex.Web
[Lazy] [Lazy]
public bool CanUpdateSettings => IsAllowed(Permissions.AppUpdateSettings); public bool CanUpdateSettings => IsAllowed(Permissions.AppUpdateSettings);
// Asset Scripts
[Lazy]
public bool CanUpdateAssetsScripts => IsAllowed(Permissions.AppAssetsScriptsUpdate);
// Contributors // Contributors
[Lazy] [Lazy]
public bool CanAssignContributor => IsAllowed(Permissions.AppContributorsAssign); public bool CanAssignContributor => IsAllowed(Permissions.AppContributorsAssign);

15
backend/src/Squidex/Areas/Api/Config/OpenApi/QueryExtensions.cs

@ -8,6 +8,7 @@
using System.Linq; using System.Linq;
using NJsonSchema; using NJsonSchema;
using NSwag; using NSwag;
using Squidex.Domain.Apps.Core;
namespace Squidex.Areas.Api.Config.OpenApi namespace Squidex.Areas.Api.Config.OpenApi
{ {
@ -43,7 +44,7 @@ namespace Squidex.Areas.Api.Config.OpenApi
{ {
Schema = stringSchema, Schema = stringSchema,
Name = "$search", Name = "$search",
Description = "Optional OData full text search." Description = FieldDescriptions.QuerySkip
}); });
} }
@ -51,42 +52,42 @@ namespace Squidex.Areas.Api.Config.OpenApi
{ {
Schema = numberSchema, Schema = numberSchema,
Name = "$top", Name = "$top",
Description = "Optional OData parameter to define the number of items to retrieve." Description = FieldDescriptions.QueryTop
}); });
AddQuery(new OpenApiParameter AddQuery(new OpenApiParameter
{ {
Schema = numberSchema, Schema = numberSchema,
Name = "$skip", Name = "$skip",
Description = "Optional OData parameter to skip items." Description = FieldDescriptions.QuerySkip
}); });
AddQuery(new OpenApiParameter AddQuery(new OpenApiParameter
{ {
Schema = stringSchema, Schema = stringSchema,
Name = "$orderby", Name = "$orderby",
Description = "Optional OData order definition to sort the result set." Description = FieldDescriptions.QueryOrderBy
}); });
AddQuery(new OpenApiParameter AddQuery(new OpenApiParameter
{ {
Schema = stringSchema, Schema = stringSchema,
Name = "$filter", Name = "$filter",
Description = "Optional OData order definition to filter the result set." Description = FieldDescriptions.QueryFilter
}); });
AddQuery(new OpenApiParameter AddQuery(new OpenApiParameter
{ {
Schema = stringSchema, Schema = stringSchema,
Name = "q", Name = "q",
Description = "JSON query as well formatted json string. Overrides all other query parameters, except 'ids'." Description = FieldDescriptions.QueryQ
}); });
AddQuery(new OpenApiParameter AddQuery(new OpenApiParameter
{ {
Schema = stringSchema, Schema = stringSchema,
Name = "ids", Name = "ids",
Description = "Comma separated list of content items. Overrides all other query parameters." Description = FieldDescriptions.QueryIds
}); });
} }
} }

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

@ -0,0 +1,90 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppAssetsController : ApiController
{
public AppAssetsController(ICommandBus commandBus)
: base(commandBus)
{
}
/// <summary>
/// Get the app asset scripts.
/// </summary>
/// <param name="app">The name of the app to get the asset scripts for.</param>
/// <returns>
/// 200 => App asset scripts returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/assets/scripts")]
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppAssetSScriptsRead)]
[ApiCosts(0)]
public IActionResult GetScripts(string app)
{
var response = Deferred.Response(() =>
{
return GetResponse(App);
});
return Ok(response);
}
/// <summary>
/// Update the app asset scripts.
/// </summary>
/// <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.
/// 404 => App not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/scripts")]
[ProducesResponseType(typeof(AssetScriptsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppAssetsScriptsUpdate)]
[ApiCosts(0)]
public async Task<IActionResult> PutScripts(string app, [FromBody] UpdateAssetScriptsDto request)
{
var response = await InvokeCommandAsync(request.ToCommand());
return Ok(response);
}
private async Task<AssetScriptsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = GetResponse(result);
return response;
}
private AssetScriptsDto GetResponse(IAppEntity result)
{
return AssetScriptsDto.FromApp(result, Resources);
}
}
}

90
backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs

@ -0,0 +1,90 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppSettingsController : ApiController
{
public AppSettingsController(ICommandBus commandBus)
: base(commandBus)
{
}
/// <summary>
/// Get the app settings.
/// </summary>
/// <param name="app">The name of the app to get the settings for.</param>
/// <returns>
/// 200 => App settingsd returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(0)]
public IActionResult GetSettings(string app)
{
var response = Deferred.Response(() =>
{
return GetResponse(App);
});
return Ok(response);
}
/// <summary>
/// Update the app settings.
/// </summary>
/// <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.
/// 404 => App not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppUpdateSettings)]
[ApiCosts(0)]
public async Task<IActionResult> PutSettings(string app, [FromBody] UpdateAppSettingsDto request)
{
var response = await InvokeCommandAsync(request.ToCommand());
return Ok(response);
}
private async Task<AppSettingsDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = GetResponse(result);
return response;
}
private AppSettingsDto GetResponse(IAppEntity result)
{
return AppSettingsDto.FromApp(result, Resources);
}
}
}

45
backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -165,51 +165,6 @@ namespace Squidex.Areas.Api.Controllers.Apps
return Ok(response); return Ok(response);
} }
/// <summary>
/// Get the app settings.
/// </summary>
/// <param name="app">The name of the app to get the settings for.</param>
/// <returns>
/// 200 => App settingsd returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(0)]
public IActionResult GetAppSettings(string app)
{
var response = Deferred.Response(() =>
{
return AppSettingsDto.FromApp(App, Resources);
});
return Ok(response);
}
/// <summary>
/// Update the app settings.
/// </summary>
/// <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.
/// 404 => App not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/settings")]
[ProducesResponseType(typeof(AppSettingsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(Permissions.AppUpdate)]
[ApiCosts(0)]
public async Task<IActionResult> PutAppSettings(string app, [FromBody] UpdateAppSettingsDto request)
{
var response = await InvokeCommandAsync(request.ToCommand(), x => AppSettingsDto.FromApp(x, Resources));
return Ok(response);
}
/// <summary> /// <summary>
/// Upload the app image. /// Upload the app image.
/// </summary> /// </summary>

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

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

4
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/AppSettingsDto.cs

@ -61,11 +61,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
var values = new { app = resources.App }; var values = new { app = resources.App };
AddSelfLink(resources.Url<AppsController>(x => nameof(x.GetAppSettings), values)); AddSelfLink(resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values));
if (resources.CanUpdateSettings) if (resources.CanUpdateSettings)
{ {
AddPutLink("update", resources.Url<AppsController>(x => nameof(x.PutAppSettings), values)); AddPutLink("update", resources.Url<AppSettingsController>(x => nameof(x.PutSettings), values));
} }
return this; return this;

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

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Reflection;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AssetScriptsDto : Resource
{
/// <summary>
/// The script that is executed when creating an asset.
/// </summary>
public string? Create { get; init; }
/// <summary>
/// The script that is executed when updating a content.
/// </summary>
public string? Update { get; init; }
/// <summary>
/// The script that is executed when annotating a content.
/// </summary>
public string? Annotate { get; init; }
/// <summary>
/// The script that is executed when moving a content.
/// </summary>
public string? Move { get; init; }
/// <summary>
/// The script that is executed when deleting a content.
/// </summary>
public string? Delete { get; init; }
/// <summary>
/// The version of the app.
/// </summary>
public long Version { get; set; }
public static AssetScriptsDto FromApp(IAppEntity app, Resources resources)
{
var result = SimpleMapper.Map(app.AssetScripts, new AssetScriptsDto());
return result.CreateLinks(resources);
}
private AssetScriptsDto CreateLinks(Resources resources)
{
var values = new { app = resources.App };
AddSelfLink(resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values));
if (resources.CanUpdateAssetsScripts)
{
AddPutLink("update", resources.Url<AppAssetsController>(x => nameof(x.PutScripts), values));
}
return this;
}
}
}

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

@ -46,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
Editors = Editors?.Select(x => x.ToEditor()).ToImmutableList()!, Editors = Editors?.Select(x => x.ToEditor()).ToImmutableList()!,
HideScheduler = HideScheduler, HideScheduler = HideScheduler,
HideDateTimeModeButton = HideDateTimeModeButton, HideDateTimeModeButton = HideDateTimeModeButton,
Patterns = Patterns?.Select(x => x.ToPattern()).ToImmutableList()!, Patterns = Patterns?.Select(x => x.ToPattern()).ToImmutableList()!
} }
}; };
} }

48
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAssetScripts.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class UpdateAssetScriptsDto
{
/// <summary>
/// The script that is executed when creating an asset.
/// </summary>
public string? Create { get; init; }
/// <summary>
/// The script that is executed when updating a content.
/// </summary>
public string? Update { get; init; }
/// <summary>
/// The script that is executed when annotating a content.
/// </summary>
public string? Annotate { get; init; }
/// <summary>
/// The script that is executed when moving a content.
/// </summary>
public string? Move { get; init; }
/// <summary>
/// The script that is executed when deleting a content.
/// </summary>
public string? Delete { get; init; }
public ConfigureAssetScripts ToCommand()
{
var scripts = SimpleMapper.Map(this, new AssetScripts());
return new ConfigureAssetScripts { Scripts = scripts };
}
}
}

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

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Assets; using Squidex.Assets;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
@ -18,6 +19,7 @@ using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Plans; using Squidex.Domain.Apps.Entities.Apps.Plans;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
@ -359,6 +361,19 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NoContent(); return NoContent();
} }
[HttpGet]
[Route("apps/{app}/assets/completion")]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
[OpenApiIgnore]
public IActionResult GetScriptCompletion(string app, string schema)
{
var completer = new ScriptingCompletion();
var completion = completer.Asset();
return Ok(completion);
}
private async Task<AssetDto> InvokeCommandAsync(ICommand command) private async Task<AssetDto> InvokeCommandAsync(ICommand command)
{ {
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);

5
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/BulkUpdateAssetsDto.cs

@ -30,6 +30,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary> /// </summary>
public bool OptimizeValidation { get; set; } = true; public bool OptimizeValidation { get; set; } = true;
/// <summary>
/// True to turn off scripting for faster inserts. Default: true.
/// </summary>
public bool DoNotScript { get; set; } = true;
public BulkUpdateAssets ToCommand() public BulkUpdateAssets ToCommand()
{ {
var result = SimpleMapper.Map(this, new BulkUpdateAssets()); var result = SimpleMapper.Map(this, new BulkUpdateAssets());

13
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/Builder.cs

@ -11,6 +11,7 @@ using NJsonSchema;
using NSwag; using NSwag;
using NSwag.Generation; using NSwag.Generation;
using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
@ -50,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
var contentSchema = ResolveSchema("ContentDto", () => var contentSchema = ResolveSchema("ContentDto", () =>
{ {
return ContentJsonSchemaBuilder.BuildSchema("Shared", dataSchema, true); return ContentJsonSchemaBuilder.BuildSchema(dataSchema, true);
}); });
var contentsSchema = ResolveSchema("ContentResultDto", () => var contentsSchema = ResolveSchema("ContentResultDto", () =>
@ -69,7 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
Parent = this, Parent = this,
SchemaDisplayName = "__Shared", SchemaDisplayName = "__Shared",
SchemaName = "__Shared", SchemaName = "__Shared",
SchemaTypeName = "__Shared", SchemaTypeName = "__Shared"
}; };
var description = "API endpoints for operations across all schemas."; var description = "API endpoints for operations across all schemas.";
@ -102,7 +103,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
}); });
} }
return ContentJsonSchemaBuilder.BuildSchema(displayName, contentDataSchema, true); return ContentJsonSchemaBuilder.BuildSchema(contentDataSchema, true);
}); });
var contentsSchema = ResolveSchema($"{typeName}ContentResultDto", () => var contentsSchema = ResolveSchema($"{typeName}ContentResultDto", () =>
@ -148,8 +149,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
AllowAdditionalProperties = false, AllowAdditionalProperties = false,
Properties = Properties =
{ {
[ResultTotal] = SchemaBuilder.NumberProperty("The total number of content items.", true), [ResultTotal] = SchemaBuilder.NumberProperty(
[ResultItems] = SchemaBuilder.ArrayProperty(contentSchema, "The content items.", true) FieldDescriptions.ContentsTotal, true),
[ResultItems] = SchemaBuilder.ArrayProperty(contentSchema,
FieldDescriptions.ContentsItems, true)
}, },
Type = JsonObjectType.Object Type = JsonObjectType.Object
}; };

3
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs

@ -9,6 +9,7 @@ using System.Collections.Generic;
using NJsonSchema; using NJsonSchema;
using NSwag; using NSwag;
using Squidex.Areas.Api.Config.OpenApi; using Squidex.Areas.Api.Config.OpenApi;
using Squidex.Domain.Apps.Core;
using Squidex.Shared; using Squidex.Shared;
using Squidex.Web; using Squidex.Web;
@ -54,7 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
public OperationBuilder HasId() public OperationBuilder HasId()
{ {
HasPath("id", JsonObjectType.String, $"The id of the schema content item."); HasPath("id", JsonObjectType.String, FieldDescriptions.EntityId);
Responds(404, "Content item not found."); Responds(404, "Content item not found.");

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

@ -6,18 +6,17 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Scripting;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Shared; using Squidex.Shared;
@ -340,11 +339,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
public IActionResult GetScriptCompletion(string app, string schema) public IActionResult GetScriptCompletion(string app, string schema)
{ {
var completer = new ScriptingCompletion(); var completer = new ScriptingCompletion();
var completion = completer.GetCompletion(Schema.SchemaDef, App.PartitionResolver()); var completion = completer.Content(Schema.SchemaDef, App.PartitionResolver());
var result = completion.Select(x => new { x.Name, x.Description }); return Ok(completion);
return Ok(result);
} }
private Task<ISchemaEntity?> GetSchemaAsync(string schema) private Task<ISchemaEntity?> GetSchemaAsync(string schema)

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

@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_return_original_content_if_script_failed() public async Task TransformAsync_should_return_original_content_if_script_failed()
{ {
var content = new ContentData(); var content = new ContentData();
var context = new ScriptVars { Data = content }; var context = new ScriptVars { ["data"] = content };
const string script = @" const string script = @"
x => x x => x
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddInvariant(10.0)); .AddInvariant(10.0));
var context = new ScriptVars { Data = content }; var context = new ScriptVars { ["data"] = content };
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -151,7 +151,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_throw_exception_if_script_failed() public async Task TransformAsync_should_throw_exception_if_script_failed()
{ {
var content = new ContentData(); var content = new ContentData();
var context = new ScriptVars { Data = content }; var context = new ScriptVars { ["data"] = content };
const string script = @" const string script = @"
invalid(); invalid();
@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_return_original_content_if_not_replaced() public async Task TransformAsync_should_return_original_content_if_not_replaced()
{ {
var content = new ContentData(); var content = new ContentData();
var context = new ScriptVars { Data = content }; var context = new ScriptVars { ["data"] = content };
const string script = @" const string script = @"
var x = 0; var x = 0;
@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_return_original_content_if_not_replaced_async() public async Task TransformAsync_should_return_original_content_if_not_replaced_async()
{ {
var content = new ContentData(); var content = new ContentData();
var context = new ScriptVars { Data = content }; var context = new ScriptVars { ["data"] = content };
const string script = @" const string script = @"
async = true; async = true;
@ -207,7 +207,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddInvariant("MyOperation")); .AddInvariant("MyOperation"));
var context = new ScriptVars { Data = content, Operation = "MyOperation" }; var context = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
["operation"] = "MyOperation"
};
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -233,7 +238,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddInvariant(42)); .AddInvariant(42));
var context = new ScriptVars { Data = content, Operation = "MyOperation" }; var context = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
["operation"] = "MyOperation"
};
const string script = @" const string script = @"
async = true; async = true;
@ -257,7 +267,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_not_ignore_transformation_if_async_not_set() public async Task TransformAsync_should_not_ignore_transformation_if_async_not_set()
{ {
var content = new ContentData(); var content = new ContentData();
var context = new ScriptVars { Data = content, Operation = "MyOperation" }; var context = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
["operation"] = "MyOperation"
};
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -279,7 +294,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_timeout_if_replace_never_called() public async Task TransformAsync_should_timeout_if_replace_never_called()
{ {
var content = new ContentData(); var content = new ContentData();
var context = new ScriptVars { Data = content, Operation = "MyOperation" }; var context = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
["operation"] = "MyOperation"
};
const string script = @" const string script = @"
async = true; async = true;
@ -314,7 +334,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData() new ContentFieldData()
.AddInvariant(10.0)); .AddInvariant(10.0));
var context = new ScriptVars { Data = content }; var context = new ScriptVars { ["data"] = content };
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
@ -358,10 +378,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2")); userIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, "2"));
var context = new ScriptVars { Data = content, DataOld = oldContent, User = userPrincipal }; var context = new ScriptVars
{
["data"] = content,
["dataOld"] = oldContent,
["user"] = userPrincipal
};
const string script = @" const string script = @"
ctx.data.number0.iv = ctx.data.number0.iv + ctx.oldData.number0.iv * parseInt(ctx.user.id, 10); ctx.data.number0.iv = ctx.data.number0.iv + ctx.dataOld.number0.iv * parseInt(ctx.user.id, 10);
replace(ctx.data); replace(ctx.data);
"; ";

14
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs

@ -23,8 +23,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "1")); identity.AddClaim(new Claim(OpenIdClaims.ClientId, "1"));
Assert.Equal("1", GetValue(identity, "user.id")); AssetUser(identity, "1", true, false);
Assert.Equal(true, GetValue(identity, "user.isClient"));
} }
[Fact] [Fact]
@ -33,9 +32,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
var identity = new ClaimsIdentity(); var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(OpenIdClaims.Subject, "2")); identity.AddClaim(new Claim(OpenIdClaims.Subject, "2"));
identity.AddClaim(new Claim(OpenIdClaims.Name, "user"));
Assert.Equal("2", GetValue(identity, "user.id")); AssetUser(identity, "2", false, true);
Assert.Equal(false, GetValue(identity, "user.isClient"));
} }
[Fact] [Fact]
@ -82,6 +81,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
Assert.Equal(new[] { "2a", "2b" }, GetValue(identity, "user.claims.claim2")); Assert.Equal(new[] { "2a", "2b" }, GetValue(identity, "user.claims.claim2"));
} }
private static void AssetUser(ClaimsIdentity identity, string id, bool isClient, bool isUser)
{
Assert.Equal(id, GetValue(identity, "user.id"));
Assert.Equal(isUser, GetValue(identity, "user.isUser"));
Assert.Equal(isClient, GetValue(identity, "user.isClient"));
}
private static object GetValue(ClaimsIdentity identity, string script) private static object GetValue(ClaimsIdentity identity, string script)
{ {
var engine = new Engine(); var engine = new Engine();

16
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs

@ -351,7 +351,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId), A<CancellationToken>._)) A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, asset)); .Returns(ResultList.CreateFrom(2, asset));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; var vars = new ScriptVars
{
["data"] = data,
["appId"] = appId.Id,
["appName"] = appId.Name,
["user"] = user
};
return (vars, asset); return (vars, asset);
} }
@ -375,7 +381,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId1, assetId2), A<CancellationToken>._)) A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId1, assetId2), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(2, asset1, asset2)); .Returns(ResultList.CreateFrom(2, asset1, asset2));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; var vars = new ScriptVars
{
["data"] = data,
["appId"] = appId.Id,
["appName"] = appId.Name,
["user"] = user
};
return (vars, new[] { asset1, asset2 }); return (vars, new[] { asset1, asset2 });
} }

88
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs

@ -10,9 +10,12 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Assets; using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
@ -26,9 +29,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{ {
public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject.State> public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject.State>
{ {
private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly ITagService tagService = A.Fake<ITagService>(); private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly DomainId parentId = DomainId.NewGuid(); private readonly DomainId parentId = DomainId.NewGuid();
private readonly DomainId assetId = DomainId.NewGuid(); private readonly DomainId assetId = DomainId.NewGuid();
private readonly AssetFile file = new NoopAssetFile(); private readonly AssetFile file = new NoopAssetFile();
@ -41,13 +47,42 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
public AssetDomainObjectTests() public AssetDomainObjectTests()
{ {
app = Mocks.App(AppNamedId, Language.DE);
var scripts = new AssetScripts
{
Annotate = "<annotate-script>",
Create = "<create-script>",
Delete = "<delete-script>",
Move = "<move-script>",
Update = "<update-script>"
};
A.CallTo(() => app.AssetScripts)
.Returns(scripts);
A.CallTo(() => appProvider.GetAppAsync(AppId, false))
.Returns(app);
A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._)) A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._))
.Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() }); .Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>._, A<HashSet<string>>._)) A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>._, A<HashSet<string>>._))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<HashSet<string>>(2)?.ToDictionary(x => x) ?? new Dictionary<string, string>())); .ReturnsLazily(x => Task.FromResult(x.GetArgument<HashSet<string>>(2)?.ToDictionary(x => x) ?? new Dictionary<string, string>()));
sut = new AssetDomainObject(PersistenceFactory, A.Dummy<ISemanticLog>(), tagService, assetQuery, contentRepository); var log = A.Fake<ISemanticLog>();
var serviceProvider =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
.AddSingleton(contentRepository)
.AddSingleton(log)
.AddSingleton(scriptEngine)
.AddSingleton(tagService)
.BuildServiceProvider();
sut = new AssetDomainObject(PersistenceFactory, log, serviceProvider);
sut.Setup(Id); sut.Setup(Id);
} }
@ -86,6 +121,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
Slug = file.FileName.ToAssetSlug() Slug = file.FileName.ToAssetSlug()
}) })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -136,6 +174,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
Slug = file.FileName.ToAssetSlug() Slug = file.FileName.ToAssetSlug()
}) })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -163,6 +204,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
MimeType = file.MimeType MimeType = file.MimeType
}) })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -190,6 +234,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
MimeType = file.MimeType MimeType = file.MimeType
}) })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -209,6 +256,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { FileName = command.FileName }) CreateAssetEvent(new AssetAnnotated { FileName = command.FileName })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -228,6 +278,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { Slug = command.Slug }) CreateAssetEvent(new AssetAnnotated { Slug = command.Slug })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -247,6 +300,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { IsProtected = command.IsProtected }) CreateAssetEvent(new AssetAnnotated { IsProtected = command.IsProtected })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -266,6 +322,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { Metadata = command.Metadata }) CreateAssetEvent(new AssetAnnotated { Metadata = command.Metadata })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<anootate-script>", ScriptOptions(), default))
.MustNotHaveHappened();
} }
[Fact] [Fact]
@ -283,6 +342,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { Tags = new HashSet<string> { "tag1" } }) CreateAssetEvent(new AssetAnnotated { Tags = new HashSet<string> { "tag1" } })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -302,6 +364,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetMoved { ParentId = parentId }) CreateAssetEvent(new AssetMoved { ParentId = parentId })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<move-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -322,6 +387,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 })
); );
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -332,7 +400,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
await ExecuteCreateAsync(); await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._)) A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._))
.Returns(true); .Returns(false);
var result = await PublishAsync(command); var result = await PublishAsync(command);
@ -340,6 +408,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version); Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version);
Assert.Empty(LastEvents); Assert.Empty(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
[Fact] [Fact]
@ -353,6 +424,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.Returns(true); .Returns(true);
await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command)); await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command));
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustNotHaveHappened();
} }
[Fact] [Fact]
@ -366,6 +440,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.Returns(true); .Returns(true);
await PublishAsync(command); await PublishAsync(command);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
} }
private Task ExecuteCreateAsync() private Task ExecuteCreateAsync()
@ -383,6 +460,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
return PublishAsync(new DeleteAsset { Permanent = permanent }); return PublishAsync(new DeleteAsset { Permanent = permanent });
} }
private static ScriptOptions ScriptOptions()
{
return A<ScriptOptions>.That.Matches(x => x.CanDisallow && x.CanReject && x.AsContext);
}
private T CreateAssetEvent<T>(T @event) where T : AssetEvent private T CreateAssetEvent<T>(T @event) where T : AssetEvent
{ {
@event.AssetId = assetId; @event.AssetId = assetId;

23
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs

@ -9,7 +9,10 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -20,7 +23,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{ {
public class AssetFolderDomainObjectTests : HandlerTestBase<AssetFolderDomainObject.State> public class AssetFolderDomainObjectTests : HandlerTestBase<AssetFolderDomainObject.State>
{ {
private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly DomainId parentId = DomainId.NewGuid(); private readonly DomainId parentId = DomainId.NewGuid();
private readonly DomainId assetFolderId = DomainId.NewGuid(); private readonly DomainId assetFolderId = DomainId.NewGuid();
private readonly AssetFolderDomainObject sut; private readonly AssetFolderDomainObject sut;
@ -32,10 +38,25 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
public AssetFolderDomainObjectTests() public AssetFolderDomainObjectTests()
{ {
app = Mocks.App(AppNamedId, Language.DE);
A.CallTo(() => appProvider.GetAppAsync(AppId, false))
.Returns(app);
A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._)) A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._))
.Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() }); .Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
sut = new AssetFolderDomainObject(PersistenceFactory, A.Dummy<ISemanticLog>(), assetQuery); var log = A.Fake<ISemanticLog>();
var serviceProvider =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
.AddSingleton(contentRepository)
.AddSingleton(log)
.BuildServiceProvider();
sut = new AssetFolderDomainObject(PersistenceFactory, log, serviceProvider);
sut.Setup(Id); sut.Setup(Id);
} }

144
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs

@ -6,9 +6,11 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -22,136 +24,138 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{ {
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly RefToken actor = RefToken.User("123");
[Fact] [Fact]
public async Task CanCreate_should_throw_exception_if_folder_name_not_defined() public void Should_throw_exception_if_folder_name_not_defined()
{ {
var command = new CreateAssetFolder { AppId = appId }; var operation = Operation(CreateAssetFolder());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, A<CancellationToken>._)) ValidationAssert.Throws(() => operation.MustHaveName(null!),
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery),
new ValidationError("Folder name is required.", "FolderName")); new ValidationError("Folder name is required.", "FolderName"));
} }
[Fact] [Fact]
public async Task CanCreate_should_throw_exception_if_folder_not_found() public void Should_not_throw_exception_if_folder_name_defined()
{ {
var command = new CreateAssetFolder { AppId = appId, FolderName = "My Folder", ParentId = DomainId.NewGuid() }; var operation = Operation(CreateAssetFolder());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, A<CancellationToken>._)) operation.MustHaveName("Folder");
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery),
new ValidationError("Asset folder does not exist.", "ParentId"));
} }
[Fact] [Fact]
public async Task CanCreate_should_not_throw_exception_if_folder_found() public async Task Should_throw_exception_if_moving_to_invalid_folder()
{ {
var command = new CreateAssetFolder { AppId = appId, FolderName = "My Folder", ParentId = DomainId.NewGuid() }; var parentId = DomainId.NewGuid();
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, A<CancellationToken>._)) var operation = Operation(CreateAssetFolder());
.Returns(new List<IAssetFolderEntity> { AssetFolder() });
await GuardAssetFolder.CanCreate(command, assetQuery); A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._))
} .Returns(new List<IAssetFolderEntity>());
[Fact]
public async Task CanCreate_should_not_throw_exception_if_added_to_root()
{
var command = new CreateAssetFolder { AppId = appId, FolderName = "My Folder" };
await GuardAssetFolder.CanCreate(command, assetQuery); await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId),
new ValidationError("Asset folder does not exist.", "ParentId"));
} }
[Fact] [Fact]
public async Task CanMove_should_throw_exception_if_adding_to_its_own_child() public async Task Should_not_throw_exception_if_moving_to_valid_folder()
{ {
var id = DomainId.NewGuid(); var parentId = DomainId.NewGuid();
var command = new MoveAssetFolder { AppId = appId, ParentId = DomainId.NewGuid() }; var operation = Operation(CreateAssetFolder());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, A<CancellationToken>._)) A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._))
.Returns(new List<IAssetFolderEntity> .Returns(new List<IAssetFolderEntity> { CreateAssetFolder() });
{
AssetFolder(id),
AssetFolder(command.ParentId)
});
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, AssetFolder(id), assetQuery), await operation.MustMoveToValidFolder(parentId);
new ValidationError("Cannot add folder to its own child.", "ParentId"));
} }
[Fact] [Fact]
public async Task CanMove_should_throw_exception_if_folder_not_found() public async Task Should_not_throw_exception_if_moving_to_same_folder()
{ {
var command = new MoveAssetFolder { AppId = appId, ParentId = DomainId.NewGuid() }; var parentId = DomainId.NewGuid();
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, A<CancellationToken>._)) var operation = Operation(CreateAssetFolder(default, parentId));
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, AssetFolder(), assetQuery), await operation.MustMoveToValidFolder(parentId);
new ValidationError("Asset folder does not exist.", "ParentId"));
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.MustNotHaveHappened();
} }
[Fact] [Fact]
public async Task CanMove_should_not_throw_exception_if_folder_found() public async Task Should_not_throw_exception_if_moving_to_root()
{ {
var command = new MoveAssetFolder { AppId = appId, ParentId = DomainId.NewGuid() }; var parentId = DomainId.Empty;
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, A<CancellationToken>._)) var operation = Operation(CreateAssetFolder());
.Returns(new List<IAssetFolderEntity> { AssetFolder() });
await GuardAssetFolder.CanMove(command, AssetFolder(), assetQuery); await operation.MustMoveToValidFolder(parentId);
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, A<DomainId>._, A<CancellationToken>._))
.MustNotHaveHappened();
} }
[Fact] [Fact]
public async Task CanMove_should_not_throw_exception_if_folder_has_not_changed() public async Task Should_throw_exception_if_moving_its_own_child()
{ {
var command = new MoveAssetFolder { AppId = appId, ParentId = DomainId.NewGuid() }; var parentId = DomainId.NewGuid();
await GuardAssetFolder.CanMove(command, AssetFolder(parentId: command.ParentId), assetQuery); var operation = Operation(CreateAssetFolder());
}
[Fact] A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._))
public async Task CanMove_should_not_throw_exception_if_added_to_root() .Returns(new List<IAssetFolderEntity>
{ {
var command = new MoveAssetFolder { AppId = appId }; CreateAssetFolder(operation.CommandId),
CreateAssetFolder(parentId, operation.CommandId)
});
await GuardAssetFolder.CanMove(command, AssetFolder(), assetQuery); await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId),
new ValidationError("Cannot add folder to its own child.", "ParentId"));
} }
[Fact] private AssetFolderOperation Operation(IAssetFolderEntity assetFolder)
public void CanRename_should_throw_exception_if_folder_name_is_empty()
{ {
var command = new RenameAssetFolder { AppId = appId }; return Operation(assetFolder, Mocks.FrontendUser());
ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command),
new ValidationError("Folder name is required.", "FolderName"));
} }
[Fact] private AssetFolderOperation Operation(IAssetFolderEntity assetFolder, ClaimsPrincipal? currentUser)
public void CanRename_should_not_throw_exception_if_names_are_different()
{ {
var command = new RenameAssetFolder { AppId = appId, FolderName = "New Folder Name" }; var serviceProvider =
new ServiceCollection()
GuardAssetFolder.CanRename(command); .AddSingleton(assetQuery)
.BuildServiceProvider();
return new AssetFolderOperation(serviceProvider, () => assetFolder)
{
App = Mocks.App(appId),
CommandId = assetFolder.Id,
Command = new CreateAssetFolder { User = currentUser, Actor = actor }
};
} }
private IAssetFolderEntity AssetFolder(DomainId id = default, DomainId parentId = default) private IAssetFolderEntity CreateAssetFolder(DomainId id = default, DomainId parentId = default)
{ {
var assetFolder = A.Fake<IAssetFolderEntity>(); var assetFolder = A.Fake<IAssetFolderEntity>();
A.CallTo(() => assetFolder.Id) A.CallTo(() => assetFolder.Id)
.Returns(id == default ? DomainId.NewGuid() : id); .Returns(OrNew(id));
A.CallTo(() => assetFolder.AppId) A.CallTo(() => assetFolder.AppId)
.Returns(appId); .Returns(appId);
A.CallTo(() => assetFolder.ParentId) A.CallTo(() => assetFolder.ParentId)
.Returns(parentId == default ? DomainId.NewGuid() : parentId); .Returns(OrNew(parentId));
return assetFolder; return assetFolder;
} }
private static DomainId OrNew(DomainId parentId)
{
if (parentId == default)
{
parentId = DomainId.NewGuid();
}
return parentId;
}
} }
} }

116
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs

@ -6,8 +6,10 @@
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
@ -24,112 +26,138 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly RefToken actor = RefToken.User("123");
[Fact] [Fact]
public async Task CanMove_should_throw_exception_if_folder_not_found() public async Task Should_throw_exception_if_moving_to_invalid_folder()
{ {
var parentId = DomainId.NewGuid(); var parentId = DomainId.NewGuid();
var command = new MoveAsset { AppId = appId, ParentId = parentId }; var operation = Operation(CreateAsset());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.Returns(new List<IAssetFolderEntity>()); .Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAsset.CanMove(command, Asset(), assetQuery), await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId),
new ValidationError("Asset folder does not exist.", "ParentId")); new ValidationError("Asset folder does not exist.", "ParentId"));
} }
[Fact] [Fact]
public async Task CanMove_should_not_throw_exception_if_folder_not_found_but_optimized() public async Task Should_not_throw_exception_if_moving_to_valid_folder()
{ {
var parentId = DomainId.NewGuid(); var parentId = DomainId.NewGuid();
var command = new MoveAsset { AppId = appId, ParentId = parentId, OptimizeValidation = true }; var operation = Operation(CreateAsset());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, default)) A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.Returns(new List<IAssetFolderEntity>()); .Returns(new List<IAssetFolderEntity> { CreateAssetFolder() });
await GuardAsset.CanMove(command, Asset(), assetQuery); await operation.MustMoveToValidFolder(parentId);
} }
[Fact] [Fact]
public async Task CanMove_should_not_throw_exception_if_folder_found() public async Task Should_not_throw_exception_if_moving_to_same_folder()
{ {
var parentId = DomainId.NewGuid(); var parentId = DomainId.NewGuid();
var command = new MoveAsset { AppId = appId, ParentId = parentId }; var operation = Operation(CreateAsset(default, parentId));
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, default)) await operation.MustMoveToValidFolder(parentId);
.Returns(new List<IAssetFolderEntity> { AssetFolder() });
await GuardAsset.CanMove(command, Asset(), assetQuery); A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.MustNotHaveHappened();
} }
[Fact] [Fact]
public async Task CanMove_should_not_throw_exception_if_folder_has_not_changed() public async Task Should_not_throw_exception_if_moving_to_root()
{ {
var parentId = DomainId.NewGuid(); var parentId = DomainId.Empty;
var operation = Operation(CreateAsset(parentId));
var command = new MoveAsset { AppId = appId, ParentId = parentId }; await operation.MustMoveToValidFolder(parentId);
await GuardAsset.CanMove(command, Asset(parentId: parentId), assetQuery); A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.MustNotHaveHappened();
} }
[Fact] [Fact]
public async Task CanMove_should_not_throw_exception_if_added_to_root() public async Task Should_throw_exception_if_referenced()
{ {
var command = new MoveAsset { AppId = appId }; var operation = Operation(CreateAsset());
A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default))
.Returns(true);
await GuardAsset.CanMove(command, Asset(), assetQuery); await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync());
} }
[Fact] [Fact]
public async Task CanDelete_should_throw_exception_if_referenced() public async Task Should_not_throw_exception_if_not_referenced()
{ {
var asset = Asset(); var operation = Operation(CreateAsset());
var command = new DeleteAsset { AppId = appId, CheckReferrers = true }; A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default))
A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, asset.Id, SearchScope.All, default))
.Returns(true); .Returns(true);
await Assert.ThrowsAsync<DomainException>(() => GuardAsset.CanDelete(command, asset, contentRepository)); await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync());
} }
[Fact] private AssetOperation Operation(AssetEntity asset)
public async Task CanDelete_should_not_throw_exception()
{ {
var command = new DeleteAsset { AppId = appId }; return Operation(asset, Mocks.FrontendUser());
await GuardAsset.CanDelete(command, Asset(), contentRepository);
} }
private IAssetEntity Asset(DomainId id = default, DomainId parentId = default) private AssetOperation Operation(AssetEntity asset, ClaimsPrincipal? currentUser)
{ {
var asset = A.Fake<IAssetEntity>(); var serviceProvider =
new ServiceCollection()
A.CallTo(() => asset.Id) .AddSingleton(contentRepository)
.Returns(id == default ? DomainId.NewGuid() : id); .AddSingleton(assetQuery)
A.CallTo(() => asset.AppId) .BuildServiceProvider();
.Returns(appId);
A.CallTo(() => asset.ParentId) return new AssetOperation(serviceProvider, () => asset)
.Returns(parentId == default ? DomainId.NewGuid() : parentId); {
App = Mocks.App(appId),
CommandId = asset.Id,
Command = new CreateAsset { User = currentUser, Actor = actor }
};
}
return asset; private AssetEntity CreateAsset(DomainId id = default, DomainId parentId = default)
{
return new AssetEntity
{
Id = OrNew(id),
AppId = appId,
Created = default,
CreatedBy = actor,
ParentId = OrNew(parentId)
};
} }
private IAssetFolderEntity AssetFolder(DomainId id = default, DomainId parentId = default) private IAssetFolderEntity CreateAssetFolder(DomainId id = default, DomainId parentId = default)
{ {
var assetFolder = A.Fake<IAssetFolderEntity>(); var assetFolder = A.Fake<IAssetFolderEntity>();
A.CallTo(() => assetFolder.Id) A.CallTo(() => assetFolder.Id)
.Returns(id == default ? DomainId.NewGuid() : id); .Returns(OrNew(id));
A.CallTo(() => assetFolder.AppId) A.CallTo(() => assetFolder.AppId)
.Returns(appId); .Returns(appId);
A.CallTo(() => assetFolder.ParentId) A.CallTo(() => assetFolder.ParentId)
.Returns(parentId == default ? DomainId.NewGuid() : parentId); .Returns(OrNew(parentId));
return assetFolder; return assetFolder;
} }
private static DomainId OrNew(DomainId parentId)
{
if (parentId == default)
{
parentId = DomainId.NewGuid();
}
return parentId;
}
} }
} }

115
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/ScriptMetadataWrapperTests.cs

@ -0,0 +1,115 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards
{
public class ScriptMetadataWrapperTests
{
private readonly AssetMetadata metadata = new AssetMetadata();
private readonly ScriptMetadataWrapper sut;
public ScriptMetadataWrapperTests()
{
sut = new ScriptMetadataWrapper(metadata);
}
[Fact]
public void Should_add_value()
{
sut.Add("key", 1);
Assert.Equal(JsonValue.Create(1), metadata["key"]);
Assert.Equal(JsonValue.Create(1), sut["key"]);
Assert.True(metadata.ContainsKey("key"));
Assert.True(sut.ContainsKey("key"));
Assert.Single(metadata);
Assert.Single(sut);
}
[Fact]
public void Should_set_value()
{
sut["key"] = 1;
Assert.Equal(JsonValue.Create(1), metadata["key"]);
Assert.Equal(JsonValue.Create(1), sut["key"]);
Assert.True(metadata.ContainsKey("key"));
Assert.True(sut.ContainsKey("key"));
Assert.Single(metadata);
Assert.Single(sut);
}
[Fact]
public void Should_provide_keys()
{
sut["key1"] = 1;
sut["key2"] = 2;
Assert.Equal(new[]
{
"key1",
"key2"
}, sut.Keys.ToArray());
}
[Fact]
public void Should_provide_values()
{
sut["key1"] = 1;
sut["key2"] = 2;
Assert.Equal(new object[]
{
JsonValue.Create(1),
JsonValue.Create(2)
}, sut.Values.ToArray());
}
[Fact]
public void Should_enumerate_values()
{
sut["key1"] = 1;
sut["key2"] = 2;
Assert.Equal(new[]
{
new KeyValuePair<string, object?>("key1", JsonValue.Create(1)),
new KeyValuePair<string, object?>("key2", JsonValue.Create(2)),
}, sut.ToArray());
}
[Fact]
public void Should_remove_value()
{
sut["key"] = 1;
sut.Remove("key");
Assert.Empty(metadata);
Assert.Empty(sut);
}
[Fact]
public void Should_clear_collection()
{
sut["key"] = 1;
sut.Clear();
Assert.Empty(metadata);
Assert.Empty(sut);
}
}
}

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

Loading…
Cancel
Save