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 4 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,
MethodName = action.MethodName,
MethodPayload = requestBody,
Targets = target.Split("\n"),
Targets = target.Split("\n")
};
return (ruleDescription, ruleJob);

1
backend/i18n/frontend_en.json

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

1
backend/i18n/frontend_it.json

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

1
backend/i18n/frontend_nl.json

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

1
backend/i18n/frontend_zh.json

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

1
backend/i18n/source/frontend_en.json

@ -214,6 +214,7 @@
"common.apps": "Apps",
"common.aspectRatio": "AspectRatio",
"common.assets": "Assets",
"common.assetScripts": "Asset Scripts",
"common.back": "Back",
"common.backendError": "Backend ERROR",
"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 Category { get; private set; }
public string? Category { get; private set; }
public bool IsPublished { get; private set; }
@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Schema Update(SchemaProperties newProperties)
public Schema Update(SchemaProperties? newProperties)
{
newProperties ??= new SchemaProperties();
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Schema SetScripts(SchemaScripts newScripts)
public Schema SetScripts(SchemaScripts? newScripts)
{
newScripts ??= new SchemaScripts();
@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Schema SetFieldsInLists(FieldNames names)
public Schema SetFieldsInLists(FieldNames? names)
{
names ??= FieldNames.Empty;
@ -132,7 +132,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Schema SetFieldsInReferences(FieldNames names)
public Schema SetFieldsInReferences(FieldNames? names)
{
names ??= FieldNames.Empty;
@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Schema SetFieldRules(FieldRules rules)
public Schema SetFieldRules(FieldRules? rules)
{
rules ??= FieldRules.Empty;
@ -204,7 +204,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Schema ChangeCategory(string category)
public Schema ChangeCategory(string? category)
{
if (string.Equals(Category, category))
{
@ -218,7 +218,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
}
[Pure]
public Schema SetPreviewUrls(ImmutableDictionary<string, string> previewUrls)
public Schema SetPreviewUrls(ImmutableDictionary<string, string>? previewUrls)
{
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 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);
}
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);
}

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]
public enum SkipReason
{
None,
None = 0,
ConditionDoesNotMatch = 1 << 0,
ConditionPrecheckDoesNotMatch = 1 << 1,
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.Security.Claims;
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
@ -20,32 +22,33 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
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, user.Id, isClient, user.Email, user.Claims.DisplayName(), user.Claims);
return CreateUser(
engine,
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 isClient = string.IsNullOrWhiteSpace(id);
if (isClient)
{
id = principal.OpenIdClientId()!;
}
var token = principal.Token();
var name = principal.FindFirst(SquidexClaimTypes.DisplayName)?.Value;
return CreateUser(engine, id, isClient, principal.OpenIdEmail()!, name, principal.Claims);
return CreateUser(
engine,
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 =
allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last())
@ -53,7 +56,17 @@ namespace Squidex.Domain.Apps.Core.Scripting
x => x.Key,
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.
// ==========================================================================
using System;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
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
{
get => GetValue<ContentData?>();
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)
{
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.Id),
Filter.Eq(x => x.IndexedAppId, appId),
Filter.In(x => x.IndexedSchemaId, schemaIds),
Filter.In(x => x.IndexedSchemaId, schemaIds)
};
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
.Ascending(x => x.AppId)
.Descending(x => x.Created)
.Descending(x => x.Version)),
.Descending(x => x.Version))
}, 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.Runtime.Serialization;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure;
@ -41,6 +42,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
public AppContributors Contributors { get; set; } = AppContributors.Empty;
public AssetScripts AssetScripts { get; set; } = AssetScripts.Empty;
public LanguagesConfig Languages { get; set; } = LanguagesConfig.English;
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):
return UpdatePlan(e.ToPlan());
case AppAssetsScriptsConfigured e when Is.Change(e.Scripts, AssetScripts):
return UpdateAssetScripts(e.Scripts);
case AppPlanReset e when Plan != null:
return UpdatePlan(null);
@ -205,6 +211,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
return true;
}
private bool UpdateAssetScripts(AssetScripts? scripts)
{
AssetScripts = scripts ?? AssetScripts.Empty;
return true;
}
private bool UpdateSettings(AppSettings 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;
});
case ConfigureAssetScripts configureAssetScripts:
return UpdateReturn(configureAssetScripts, c =>
{
GuardApp.CanUpdateAssetScripts(c);
ConfigureAssetScripts(c);
return Snapshot;
});
case AssignContributor assignContributor:
return UpdateReturnAsync(assignContributor, async c =>
{
@ -345,6 +355,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject
Raise(command, new AppSettingsUpdated());
}
private void ConfigureAssetScripts(ConfigureAssetScripts command)
{
Raise(command, new AppAssetsScriptsConfigured());
}
private void UpdateClient(UpdateClient command)
{
Raise(command, new AppClientUpdated());

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

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -52,6 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards
Guard.NotNull(command, nameof(command));
}
public static void CanUpdateAssetScripts(ConfigureAssetScripts command)
{
Guard.NotNull(command, nameof(command));
}
public static void CanUpdateSettings(UpdateAppSettings 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.Assets;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Apps
@ -34,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
AppContributors Contributors { get; }
AssetScripts AssetScripts { get; }
LanguagesConfig Languages { 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)
{
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
if (!context.TryGetValue<DomainId>("appId", out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user))
if (!context.TryGetValue<ClaimsPrincipal>("user", out var user))
{
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 bool DoNotScript { get; set; }
[IgnoreDataMember]
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 OptimizeValidation { get; set; }
public CreateAsset()
{
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 bool OptimizeValidation { get; set; }
public CreateAssetFolder()
{
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 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.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
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.Assets;
using Squidex.Infrastructure;
@ -20,27 +17,19 @@ using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
using Squidex.Log;
using IAssetTagService = Squidex.Domain.Apps.Core.Tags.ITagService;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{
public sealed partial class AssetDomainObject : DomainObject<AssetDomainObject.State>
{
private readonly IContentRepository contentRepository;
private readonly IAssetTagService assetTags;
private readonly IAssetQueryService assetQuery;
private readonly IServiceProvider serviceProvider;
public AssetDomainObject(IPersistenceFactory<AssetDomainObject.State> factory, ISemanticLog log,
IAssetTagService assetTags,
IAssetQueryService assetQuery,
IContentRepository contentRepository)
IServiceProvider serviceProvider)
: base(factory, log)
{
this.assetTags = assetTags;
this.assetQuery = assetQuery;
this.contentRepository = contentRepository;
Capacity = int.MaxValue;
this.serviceProvider = serviceProvider;
}
protected override bool IsDeleted()
@ -72,18 +61,20 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case UpsertAsset upsert:
return UpsertReturnAsync(upsert, async c =>
{
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
if (Version > EtagVersion.Empty && !IsDeleted())
{
UpdateCore(c.AsUpdate());
await UpdateCore(c.AsUpdate(), operation);
}
else
{
await CreateCore(c.AsCreate());
await CreateCore(c.AsCreate(), operation);
}
if (Is.OptionalChange(Snapshot.ParentId, c.ParentId))
{
await MoveCore(c.AsMove(c.ParentId.Value));
await MoveCore(c.AsMove(c.ParentId.Value), operation);
}
return Snapshot;
@ -92,11 +83,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case CreateAsset c:
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))
{
await MoveCore(c.AsMove());
await MoveCore(c.AsMove(), operation);
}
return Snapshot;
@ -105,20 +98,19 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case AnnotateAsset c:
return UpdateReturnAsync(c, async c =>
{
if (c.Tags != null)
{
c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
}
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
Annotate(c);
await AnnotateCore(c, operation);
return Snapshot;
});
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;
});
@ -126,7 +118,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case MoveAsset move:
return UpdateReturnAsync(move, async c =>
{
await MoveCore(c);
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await MoveCore(c, operation);
return Snapshot;
});
@ -134,55 +128,98 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case DeleteAsset delete when delete.Permanent:
return DeletePermanentAsync(delete, async c =>
{
await DeleteCore(c);
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation);
});
case DeleteAsset delete:
return UpdateAsync(delete, async c =>
{
await DeleteCore(c);
var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await DeleteCore(c, operation);
});
default:
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)
{
create.Tags = await NormalizeTagsAsync(create.AppId.Id, create.Tags);
create.Tags = await operation.NormalizeTags(create.Tags);
}
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)

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

@ -1,4 +1,4 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -21,13 +21,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{
public sealed partial class AssetFolderDomainObject : DomainObject<AssetFolderDomainObject.State>
{
private readonly IAssetQueryService assetQuery;
private readonly IServiceProvider serviceProvider;
public AssetFolderDomainObject(IPersistenceFactory<State> factory, ISemanticLog log,
IAssetQueryService assetQuery)
IServiceProvider serviceProvider)
: base(factory, log)
{
this.assetQuery = assetQuery;
this.serviceProvider = serviceProvider;
}
protected override bool IsDeleted()
@ -54,9 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case CreateAssetFolder c:
return CreateReturnAsync(c, async create =>
{
await GuardAssetFolder.CanCreate(create, assetQuery);
Create(create);
await CreateCore(create, c);
return Snapshot;
});
@ -64,19 +62,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
case MoveAssetFolder move:
return UpdateReturnAsync(move, async c =>
{
await GuardAssetFolder.CanMove(c, Snapshot, assetQuery);
Move(c);
await MoveCore(c);
return Snapshot;
});
case RenameAssetFolder rename:
return UpdateReturn(rename, c =>
return UpdateReturnAsync(rename, async c =>
{
GuardAssetFolder.CanRename(c);
Rename(c);
await RenameCore(c);
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)
{
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)
{
if (context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
if (context.TryGetValue<DomainId>("appId", out var appId))
{
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:
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())
{
@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
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);
}
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case CreateContent createContent:
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);
@ -109,11 +109,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case ValidateContent validate:
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 operation.ValidateContentAndInputAsync(Snapshot.Data, false, Snapshot.IsPublished());
await ValidateCore(operation);
return true;
});
@ -121,14 +119,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case CreateContentDraft createDraft:
return UpdateReturnAsync(createDraft, async c =>
{
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHavePermission(Permissions.AppContentsVersionCreate);
operation.MustCreateDraft();
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
var status = await operation.GetInitialStatusAsync();
CreateDraft(c, status);
await CreateDraftCore(c, operation);
return Snapshot;
});
@ -136,12 +129,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case DeleteContentDraft deleteDraft:
return UpdateReturnAsync(deleteDraft, async c =>
{
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot);
operation.MustHavePermission(Permissions.AppContentsVersionDelete);
operation.MustDeleteDraft();
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
DeleteDraft(c);
DeleteDraftCore(c, operation);
return Snapshot;
});
@ -149,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case PatchContent patchContent:
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);
@ -159,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case UpdateContent updateContent:
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);
@ -169,14 +159,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case CancelContentSchedule cancelContentSchedule:
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);
if (Snapshot.ScheduleJob != null)
{
CancelChangeStatus(c);
}
CancelChangeCore(c, operation);
return Snapshot;
});
@ -192,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
}
else
{
var operation = await OperationContext.CreateAsync(serviceProvider, c, () => Snapshot);
var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot);
await ChangeCore(c, operation);
}
@ -215,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case DeleteContent deleteContent when deleteContent.Permanent:
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);
});
@ -223,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
case DeleteContent deleteContent:
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);
});
@ -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.MustNotCreateForUnpublishedSchema();
@ -261,7 +246,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Create(c, status);
}
private async Task ChangeCore(ChangeContentStatus c, OperationContext operation)
private async Task ChangeCore(ChangeContentStatus c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsChangeStatusOwn);
operation.MustNotChangeSingleton(c.Status);
@ -314,7 +299,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
ChangeStatus(c);
}
private async Task UpdateCore(UpdateContent c, OperationContext operation)
private async Task UpdateCore(UpdateContent c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsUpdate);
operation.MustHaveData(c.Data);
@ -349,7 +334,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
Update(c, newData);
}
private async Task PatchCore(UpdateContent c, OperationContext operation)
private async Task PatchCore(UpdateContent c, ContentOperation operation)
{
operation.MustHavePermission(Permissions.AppContentsUpdate);
operation.MustHaveData(c.Data);
@ -384,7 +369,42 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
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.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
};
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
{
Operation = "Create",
Data = data,
DataOld = default,
Status = status,
StatusOld = default
});
data = await GetScriptEngine(context).TransformAsync(vars, script, Options);
return Task.FromResult(data);
}
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
{
Operation = "Update",
Data = data,
DataOld = context.Content.Data,
Status = context.Content.EditingStatus(),
StatusOld = default
});
data = await GetScriptEngine(context).TransformAsync(vars, script, Options);
return Task.FromResult(data);
}
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();
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 Task.FromResult(operation.Snapshot.Data);
}
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
{
Operation = "Delete",
Data = context.Content.Data,
DataOld = default,
Status = context.Content.EditingStatus(),
StatusOld = default
});
await GetScriptEngine(context).ExecuteAsync(vars, script, Options);
return Task.CompletedTask;
}
var vars = Enrich(operation, new ScriptVars
{
[ScriptKeys.Data] = operation.Snapshot.Data,
[ScriptKeys.DataOld] = null,
[ScriptKeys.OldData] = null,
[ScriptKeys.OldStatus] = operation.Snapshot.EditingStatus(),
[ScriptKeys.Operation] = "Delete",
[ScriptKeys.Status] = operation.Snapshot.EditingStatus(),
[ScriptKeys.StatusOld] = default(Status)
});
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.AppId = context.App.Id;
vars.AppName = context.App.Name;
vars.SchemaId = context.Schema.Id;
vars.SchemaName = context.Schema.SchemaDef.Name;
vars.User = context.User;
vars[ScriptKeys.AppId] = operation.App.Id;
vars[ScriptKeys.AppName] = operation.App.Name;
vars[ScriptKeys.Command] = operation.Command;
vars[ScriptKeys.Content] = operation.Snapshot;
vars[ScriptKeys.ContentId] = operation.CommandId;
vars[ScriptKeys.SchemaId] = operation.Schema.Id;
vars[ScriptKeys.SchemaName] = operation.Schema.SchemaDef.Name;
vars[ScriptKeys.User] = operation.User;
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 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)
{

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 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"));
}
}
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"));
}
}
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"));
}
}
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"));
}

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 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"));
}
}
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"));
}
}
public static void MustHaveData(this OperationContext context, ContentData? data)
public static void MustHaveData(this ContentOperation operation, ContentData? data)
{
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);
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);
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);
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);
@ -85,16 +87,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
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)
{
@ -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 =
new ValidationContext(context.Resolve<IJsonSerializer>(),
context.App.NamedId(),
context.Schema.NamedId(),
context.SchemaDef,
context.Components,
context.ContentId)
new ValidationContext(operation.Resolve<IJsonSerializer>(),
operation.App.NamedId(),
operation.Schema.NamedId(),
operation.SchemaDef,
operation.Components,
operation.CommandId)
.Optimized(optimize).AsPublishing(published);
var validator =
new ContentValidator(context.Partition(),
new ContentValidator(operation.Partition(),
validationContext,
context.Resolve<IEnumerable<IValidatorsFactory>>(),
context.Resolve<ISemanticLog>());
operation.Resolve<IEnumerable<IValidatorsFactory>>(),
operation.Resolve<ISemanticLog>());
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 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 };
context.AddError(T.Get("contents.statusTransitionNotAllowed", values), nameof(status));
context.ThrowOnErrors();
operation.AddError(T.Get("contents.statusTransitionNotAllowed", values), nameof(status));
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)
{
context.AddError(T.Get("contents.statusNotValid"), nameof(status));
context.ThrowOnErrors();
operation.AddError(T.Get("contents.statusNotValid"), nameof(status));
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 }));
}
}
}
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.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
@ -22,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
new QueryArgument(AllTypes.String)
{
Name = "path",
Description = "The path to the json value",
Description = FieldDescriptions.JsonPath,
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.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
@ -28,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "id",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = "The id of the asset."
Description = FieldDescriptions.EntityId
});
AddField(new FieldType
@ -36,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "version",
ResolvedType = AllTypes.NonNullInt,
Resolver = EntityResolvers.Version,
Description = "The version of the asset."
Description = FieldDescriptions.EntityVersion
});
AddField(new FieldType
@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "created",
ResolvedType = AllTypes.NonNullDateTime,
Resolver = EntityResolvers.Created,
Description = "The date and time when the asset has been created."
Description = FieldDescriptions.EntityCreated
});
AddField(new FieldType
@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "createdBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.CreatedBy,
Description = "The user that has created the asset."
Description = FieldDescriptions.EntityCreatedBy
});
AddField(new FieldType
@ -60,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "createdByUser",
ResolvedType = UserGraphType.NonNull,
Resolver = EntityResolvers.CreatedByUser,
Description = "The full info of the user that has created the asset."
Description = FieldDescriptions.EntityCreatedBy
});
AddField(new FieldType
@ -68,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "lastModified",
ResolvedType = AllTypes.NonNullDateTime,
Resolver = EntityResolvers.LastModified,
Description = "The date and time when the asset has been modified last."
Description = FieldDescriptions.EntityLastModified
});
AddField(new FieldType
@ -76,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.LastModifiedBy,
Description = "The user that has updated the asset last."
Description = FieldDescriptions.EntityLastModifiedBy
});
AddField(new FieldType
@ -84,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "lastModifiedByUser",
ResolvedType = UserGraphType.NonNull,
Resolver = EntityResolvers.LastModifiedByUser,
Description = "The full info of the user that has created the asset."
Description = FieldDescriptions.EntityLastModifiedBy
});
AddField(new FieldType
@ -92,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "mimeType",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.MimeType),
Description = "The mime type."
Description = FieldDescriptions.AssetMimeType
});
AddField(new FieldType
@ -100,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "url",
ResolvedType = AllTypes.NonNullString,
Resolver = Url,
Description = "The url to the asset."
Description = FieldDescriptions.AssetUrl
});
AddField(new FieldType
@ -108,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "thumbnailUrl",
ResolvedType = AllTypes.String,
Resolver = ThumbnailUrl,
Description = "The thumbnail url to the asset."
Description = FieldDescriptions.AssetThumbnailUrl
});
AddField(new FieldType
@ -116,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileName",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileName),
Description = "The file name."
Description = FieldDescriptions.AssetFileName
});
AddField(new FieldType
@ -124,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileHash",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileHash),
Description = "The hash of the file. Can be null for old files."
Description = FieldDescriptions.AssetFileHash
});
AddField(new FieldType
@ -132,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileType",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileName.FileType()),
Description = "The file type."
Description = FieldDescriptions.AssetFileType
});
AddField(new FieldType
@ -140,7 +141,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileSize",
ResolvedType = AllTypes.NonNullInt,
Resolver = Resolve(x => x.FileSize),
Description = "The size of the file in bytes."
Description = FieldDescriptions.AssetFileSize
});
AddField(new FieldType
@ -148,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "fileVersion",
ResolvedType = AllTypes.NonNullInt,
Resolver = Resolve(x => x.FileVersion),
Description = "The version of the file."
Description = FieldDescriptions.AssetFileVersion
});
AddField(new FieldType
@ -156,7 +157,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "slug",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Slug),
Description = "The file name as slug."
Description = FieldDescriptions.AssetSlug
});
AddField(new FieldType
@ -164,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "isProtected",
ResolvedType = AllTypes.NonNullBoolean,
Resolver = Resolve(x => x.IsProtected),
Description = "True, when the asset is not public."
Description = FieldDescriptions.AssetIsProtected
});
AddField(new FieldType
@ -172,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "isImage",
ResolvedType = AllTypes.NonNullBoolean,
Resolver = Resolve(x => x.Type == AssetType.Image),
Description = "Determines if the uploaded file is an image.",
Description = FieldDescriptions.AssetIsImage,
DeprecationReason = "Use 'type' field instead."
});
@ -181,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "pixelWidth",
ResolvedType = AllTypes.Int,
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."
});
@ -190,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "pixelHeight",
ResolvedType = AllTypes.Int,
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."
});
@ -199,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "type",
ResolvedType = AllTypes.NonNullAssetType,
Resolver = Resolve(x => x.Type),
Description = "The type of the image."
Description = FieldDescriptions.AssetType
});
AddField(new FieldType
@ -207,7 +208,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "metadataText",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.MetadataText),
Description = "The text representation of the metadata."
Description = FieldDescriptions.AssetMetadataText
});
AddField(new FieldType
@ -215,7 +216,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "tags",
ResolvedType = AllTypes.NonNullStrings,
Resolver = Resolve(x => x.TagNames),
Description = "The asset tags."
Description = FieldDescriptions.AssetTags
});
AddField(new FieldType
@ -224,7 +225,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Arguments = AssetActions.Metadata.Arguments,
ResolvedType = AllTypes.JsonNoop,
Resolver = AssetActions.Metadata.Resolver,
Description = "The asset metadata."
Description = FieldDescriptions.AssetMetadata
});
if (canGenerateSourceUrl)
@ -234,7 +235,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "sourceUrl",
ResolvedType = AllTypes.NonNullString,
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 GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
@ -25,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "total",
ResolvedType = AllTypes.NonNullInt,
Resolver = ResolveList(x => x.Total),
Description = "The total count of assets."
Description = FieldDescriptions.AssetsTotal
});
AddField(new FieldType
@ -33,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
Name = "items",
ResolvedType = new NonNullGraphType(assetsList),
Resolver = ResolveList(x => x),
Description = "The assets."
Description = FieldDescriptions.AssetsItems
});
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.Types;
using NodaTime;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
@ -28,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String)
{
Name = "path",
Description = "The path to the json value",
Description = FieldDescriptions.JsonPath,
DefaultValue = null
}
};
@ -51,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String)
{
Name = "path",
Description = "The path to the json value",
Description = FieldDescriptions.JsonPath,
DefaultValue = null
}
};
@ -63,13 +64,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.NonNullString)
{
Name = "id",
Description = "The id of the content (usually GUID).",
Description = FieldDescriptions.EntityId,
DefaultValue = null
},
new QueryArgument(AllTypes.Int)
{
Name = "version",
Description = "The optional version of the content to retrieve an older instance (not cached).",
Description = FieldDescriptions.QueryVersion,
DefaultValue = null
}
};
@ -98,31 +99,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.Int)
{
Name = "top",
Description = "Optional number of contents to take.",
Description = FieldDescriptions.QueryTop,
DefaultValue = null
},
new QueryArgument(AllTypes.Int)
{
Name = "skip",
Description = "Optional number of contents to skip.",
Description = FieldDescriptions.QuerySkip,
DefaultValue = 0
},
new QueryArgument(AllTypes.String)
{
Name = "filter",
Description = "Optional OData filter.",
Description = FieldDescriptions.QueryFilter,
DefaultValue = null
},
new QueryArgument(AllTypes.String)
{
Name = "orderby",
Description = "Optional OData order definition.",
Description = FieldDescriptions.QueryOrderBy,
DefaultValue = null
},
new QueryArgument(AllTypes.String)
{
Name = "search",
Description = "Optional OData full text search.",
Description = FieldDescriptions.QuerySearch,
DefaultValue = null
}
};
@ -173,25 +174,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(new NonNullGraphType(inputType))
{
Name = "data",
Description = "The data for the content.",
Description = FieldDescriptions.ContentRequestData,
DefaultValue = null
},
new QueryArgument(AllTypes.Boolean)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
Description = FieldDescriptions.ContentRequestPublish,
DefaultValue = false
},
new QueryArgument(AllTypes.String)
{
Name = "status",
Description = "The initial status.",
Description = FieldDescriptions.ContentRequestOptionalStatus,
DefaultValue = null
},
new QueryArgument(AllTypes.String)
{
Name = "id",
Description = "The optional custom content id.",
Description = FieldDescriptions.ContentRequestOptionalId,
DefaultValue = null
}
};
@ -232,31 +233,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.NonNullString)
{
Name = "id",
Description = "The id of the content (usually GUID).",
Description = FieldDescriptions.EntityId,
DefaultValue = null
},
new QueryArgument(new NonNullGraphType(inputType))
{
Name = "data",
Description = "The data for the content.",
Description = FieldDescriptions.ContentRequestData,
DefaultValue = null
},
new QueryArgument(AllTypes.Boolean)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
Description = FieldDescriptions.ContentRequestPublish,
DefaultValue = false
},
new QueryArgument(AllTypes.String)
{
Name = "status",
Description = "The initial status.",
Description = FieldDescriptions.ContentRequestOptionalStatus,
DefaultValue = null
},
new QueryArgument(AllTypes.Int)
{
Name = "expectedVersion",
Description = "The expected version",
Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any
}
};
@ -294,19 +295,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String)
{
Name = "id",
Description = "The optional custom content id.",
Description = FieldDescriptions.EntityId,
DefaultValue = null
},
new QueryArgument(new NonNullGraphType(inputType))
{
Name = "data",
Description = "The data for the content.",
Description = FieldDescriptions.ContentRequestData,
DefaultValue = null
},
new QueryArgument(AllTypes.Int)
{
Name = "expectedVersion",
Description = "The expected version",
Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any
}
};
@ -330,19 +331,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.String)
{
Name = "id",
Description = "The optional custom content id.",
Description = FieldDescriptions.EntityId,
DefaultValue = null
},
new QueryArgument(new NonNullGraphType(inputType))
{
Name = "data",
Description = "The data for the content.",
Description = FieldDescriptions.ContentRequestData,
DefaultValue = null
},
new QueryArgument(AllTypes.Int)
{
Name = "expectedVersion",
Description = "The expected version",
Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any
}
};
@ -364,25 +365,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.NonNullString)
{
Name = "id",
Description = "The id of the content (usually GUID).",
Description = FieldDescriptions.EntityId,
DefaultValue = null
},
new QueryArgument(AllTypes.NonNullString)
{
Name = "status",
Description = "The new status",
Description = FieldDescriptions.ContentRequestStatus,
DefaultValue = null
},
new QueryArgument(AllTypes.DateTime)
{
Name = "dueTime",
Description = "When to change the status",
Description = FieldDescriptions.ContentRequestDueTime,
DefaultValue = null
},
new QueryArgument(AllTypes.Int)
{
Name = "expectedVersion",
Description = "The expected version",
Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any
}
};
@ -410,7 +411,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
new QueryArgument(AllTypes.Int)
{
Name = "expectedVersion",
Description = "The expected version",
Description = FieldDescriptions.EntityExpectedVersion,
DefaultValue = EtagVersion.Any
}
};

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

@ -8,6 +8,7 @@
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure.Json.Objects;
@ -21,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "id",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = "The id of the content."
Description = FieldDescriptions.EntityId
};
public static readonly FieldType Version = new FieldType
@ -29,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "version",
ResolvedType = AllTypes.NonNullInt,
Resolver = EntityResolvers.Version,
Description = "The version of the content."
Description = FieldDescriptions.EntityVersion
};
public static readonly FieldType Created = new FieldType
@ -37,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "created",
ResolvedType = AllTypes.NonNullDateTime,
Resolver = EntityResolvers.Created,
Description = "The date and time when the content has been created."
Description = FieldDescriptions.EntityCreated
};
public static readonly FieldType CreatedBy = new FieldType
@ -45,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "createdBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.CreatedBy,
Description = "The user that has created the content."
Description = FieldDescriptions.EntityCreatedBy
};
public static readonly FieldType CreatedByUser = new FieldType
@ -53,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "createdByUser",
ResolvedType = UserGraphType.NonNull,
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
@ -61,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "lastModified",
ResolvedType = AllTypes.NonNullDateTime,
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
@ -69,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.LastModifiedBy,
Description = "The user that has updated the content last."
Description = FieldDescriptions.EntityLastModifiedBy
};
public static readonly FieldType LastModifiedByUser = new FieldType
@ -77,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "lastModifiedByUser",
ResolvedType = UserGraphType.NonNull,
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
@ -85,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "status",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Status.ToString().ToUpperInvariant()),
Description = "The status of the content."
Description = FieldDescriptions.ContentStatus
};
public static readonly FieldType StatusColor = new FieldType
@ -93,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.StatusColor),
Description = "The status color of the content."
Description = FieldDescriptions.ContentStatusColor
};
public static readonly FieldType NewStatus = new FieldType
@ -101,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "newStatus",
ResolvedType = AllTypes.String,
Resolver = Resolve(x => x.NewStatus?.ToString().ToUpperInvariant()),
Description = "The new status of the content."
Description = FieldDescriptions.ContentNewStatus
};
public static readonly FieldType NewStatusColor = new FieldType
@ -109,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "newStatusColor",
ResolvedType = AllTypes.String,
Resolver = Resolve(x => x.NewStatusColor),
Description = "The new status color of the content."
Description = FieldDescriptions.ContentStatusColor
};
public static readonly FieldType SchemaId = new FieldType
@ -117,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "schemaId",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x[Component.Discriminator].ToString()),
Description = "The id of the schema."
Description = FieldDescriptions.ContentSchemaId
};
public static readonly FieldType Url = new FieldType
@ -125,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "url",
ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.Url,
Description = "The url to the content."
Description = FieldDescriptions.ContentUrl
};
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.Linq;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
@ -57,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "data",
ResolvedType = new NonNullGraphType(contentDataType),
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",
ResolvedType = new NonNullGraphType(contentDataTypeFlat),
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 Squidex.Domain.Apps.Core;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
@ -22,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "total",
ResolvedType = AllTypes.NonNullInt,
Resolver = ContentResolvers.ListTotal,
Description = $"The total number of {schemaInfo.DisplayName} items."
Description = FieldDescriptions.ContentsTotal
});
AddField(new FieldType
@ -30,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
Name = "items",
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = ContentResolvers.ListItems,
Description = $"The {schemaInfo.DisplayName} items."
Description = FieldDescriptions.ContentsItems
});
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 GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
@ -29,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "id",
Resolver = Resolve(x => x.Id),
ResolvedType = AllTypes.NonNullString,
Description = "The id of the user."
Description = FieldDescriptions.UserId
});
AddField(new FieldType
@ -37,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "displayName",
Resolver = Resolve(x => x.Claims.DisplayName()),
ResolvedType = AllTypes.String,
Description = "The display name of the user."
Description = FieldDescriptions.UserDisplayName
});
AddField(new FieldType
@ -45,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "email",
Resolver = Resolve(x => x.Email),
ResolvedType = AllTypes.String,
Description = "The email of the user."
Description = FieldDescriptions.UserEmail
});
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 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 IJsonSerializer jsonSerializer;
private readonly IAppProvider appprovider;
@ -232,6 +232,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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,
ResolvedComponents components)
{
@ -252,14 +260,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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,
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 static class ScriptKeys
{
public const string AppId = "appId";
public const string AppName = "appName";
public const string ContentId = "contentId";
public const string Data = "data";
public const string Operation = "operation";
public const string User = "user";
}
public ScriptContent(IScriptEngine scriptEngine)
{
this.scriptEngine = scriptEngine;
@ -46,11 +56,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
var vars = new ScriptVars
{
ContentId = content.Id,
Data = content.Data,
AppId = context.App.Id,
AppName = context.App.Name,
User = context.User
[ScriptKeys.AppId] = context.App.Id,
[ScriptKeys.AppName] = context.App.Name,
[ScriptKeys.ContentId] = content.Id,
[ScriptKeys.Data] = content.Data,
[ScriptKeys.User] = context.User
};
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)
{
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
if (!context.TryGetValue<DomainId>("appId", out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user))
if (!context.TryGetValue<ClaimsPrincipal>("user", out var user))
{
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 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
{
Properties =
{
["id"] = SchemaBuilder.StringProperty($"The id of the {name} content.", true),
["created"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been created.", true),
["createdBy"] = SchemaBuilder.StringProperty($"The user that has created the {name} content.", true),
["lastModified"] = SchemaBuilder.DateTimeProperty($"The date and time when the {name} content has been modified last.", true),
["lastModifiedBy"] = SchemaBuilder.StringProperty($"The user that has updated the {name} content last.", true),
["newStatus"] = SchemaBuilder.StringProperty("The new status of the content."),
["status"] = SchemaBuilder.StringProperty("The status of the content.", true),
["id"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityId, true),
["created"] = SchemaBuilder.DateTimeProperty(FieldDescriptions.EntityCreated, true),
["createdBy"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityCreatedBy, true),
["lastModified"] = SchemaBuilder.DateTimeProperty(FieldDescriptions.EntityLastModified, true),
["lastModifiedBy"] = SchemaBuilder.StringProperty(FieldDescriptions.EntityLastModifiedBy, true),
["newStatus"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentNewStatus),
["status"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentStatus, true)
},
Type = JsonObjectType.Object
};
if (withDeleted)
{
jsonSchema.Properties["isDeleted"] = SchemaBuilder.BooleanProperty("True when deleted.", false);
jsonSchema.Properties["isDeleted"] = SchemaBuilder.BooleanProperty(FieldDescriptions.EntityIsDeleted, false);
}
if (extended)
{
jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty("The color of the new status.", false);
jsonSchema.Properties["schema"] = SchemaBuilder.StringProperty("The name of the schema.", true);
jsonSchema.Properties["SchemaName"] = SchemaBuilder.StringProperty("The display name of the schema.", true);
jsonSchema.Properties["statusColor"] = SchemaBuilder.StringProperty("The color of the status.", true);
jsonSchema.Properties["newStatusColor"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentNewStatusColor, false);
jsonSchema.Properties["schema"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentSchema, true);
jsonSchema.Properties["SchemaName"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentSchemaName, true);
jsonSchema.Properties["statusColor"] = SchemaBuilder.StringProperty(FieldDescriptions.ContentStatusColor, true);
}
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;

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
{
Rule = rule,
RuleId = ruleId,
RuleId = ruleId
};
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 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 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 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))]
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))]
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))]
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))]
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))]
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)
{
}
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)
{
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)
{
Guard.NotNull(obj, nameof(obj));
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;
}

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)
{
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)
{
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()
@ -48,6 +52,13 @@ namespace Squidex.Infrastructure.Json.Objects
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)
{
if (value == null)
@ -80,6 +91,10 @@ namespace Squidex.Infrastructure.Json.Objects
return Create(i);
case Instant i:
return Create(i);
case object[] array:
return Array(array);
case IReadOnlyDictionary<string, object?> obj:
return Object(obj);
}
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 =
{
CompareOperator.LessThan,
CompareOperator.Exists,
CompareOperator.Exists
};
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 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
public const string AppRules = "squidex.apps.{app}.rules";
public const string AppRulesRead = "squidex.apps.{app}.rules.read";

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

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

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

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

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 };
AddSelfLink(resources.Url<AppsController>(x => nameof(x.GetAppSettings), values));
AddSelfLink(resources.Url<AppSettingsController>(x => nameof(x.GetSettings), values));
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;

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()!,
HideScheduler = HideScheduler,
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.Mvc;
using Microsoft.Net.Http.Headers;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Assets;
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.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Translations;
@ -359,6 +361,19 @@ namespace Squidex.Areas.Api.Controllers.Assets
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)
{
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>
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()
{
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.Generation;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
@ -50,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
var contentSchema = ResolveSchema("ContentDto", () =>
{
return ContentJsonSchemaBuilder.BuildSchema("Shared", dataSchema, true);
return ContentJsonSchemaBuilder.BuildSchema(dataSchema, true);
});
var contentsSchema = ResolveSchema("ContentResultDto", () =>
@ -69,7 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
Parent = this,
SchemaDisplayName = "__Shared",
SchemaName = "__Shared",
SchemaTypeName = "__Shared",
SchemaTypeName = "__Shared"
};
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", () =>
@ -148,8 +149,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
AllowAdditionalProperties = false,
Properties =
{
[ResultTotal] = SchemaBuilder.NumberProperty("The total number of content items.", true),
[ResultItems] = SchemaBuilder.ArrayProperty(contentSchema, "The content items.", true)
[ResultTotal] = SchemaBuilder.NumberProperty(
FieldDescriptions.ContentsTotal, true),
[ResultItems] = SchemaBuilder.ArrayProperty(contentSchema,
FieldDescriptions.ContentsItems, true)
},
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 NSwag;
using Squidex.Areas.Api.Config.OpenApi;
using Squidex.Domain.Apps.Core;
using Squidex.Shared;
using Squidex.Web;
@ -54,7 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
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.");

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

@ -6,18 +6,17 @@
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
@ -340,11 +339,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
public IActionResult GetScriptCompletion(string app, string schema)
{
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(result);
return Ok(completion);
}
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()
{
var content = new ContentData();
var context = new ScriptVars { Data = content };
var context = new ScriptVars { ["data"] = content };
const string script = @"
x => x
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(10.0));
var context = new ScriptVars { Data = content };
var context = new ScriptVars { ["data"] = content };
const string script = @"
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()
{
var content = new ContentData();
var context = new ScriptVars { Data = content };
var context = new ScriptVars { ["data"] = content };
const string script = @"
invalid();
@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public async Task TransformAsync_should_return_original_content_if_not_replaced()
{
var content = new ContentData();
var context = new ScriptVars { Data = content };
var context = new ScriptVars { ["data"] = content };
const string script = @"
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()
{
var content = new ContentData();
var context = new ScriptVars { Data = content };
var context = new ScriptVars { ["data"] = content };
const string script = @"
async = true;
@ -207,7 +207,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant("MyOperation"));
var context = new ScriptVars { Data = content, Operation = "MyOperation" };
var context = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
["operation"] = "MyOperation"
};
const string script = @"
var data = ctx.data;
@ -233,7 +238,12 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(42));
var context = new ScriptVars { Data = content, Operation = "MyOperation" };
var context = new ScriptVars
{
["data"] = content,
["dataOld"] = null,
["operation"] = "MyOperation"
};
const string script = @"
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()
{
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 = @"
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()
{
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 = @"
async = true;
@ -314,7 +334,7 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
new ContentFieldData()
.AddInvariant(10.0));
var context = new ScriptVars { Data = content };
var context = new ScriptVars { ["data"] = content };
const string script = @"
var data = ctx.data;
@ -358,10 +378,15 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
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 = @"
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);
";

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"));
Assert.Equal("1", GetValue(identity, "user.id"));
Assert.Equal(true, GetValue(identity, "user.isClient"));
AssetUser(identity, "1", true, false);
}
[Fact]
@ -33,9 +32,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(OpenIdClaims.Subject, "2"));
identity.AddClaim(new Claim(OpenIdClaims.Name, "user"));
Assert.Equal("2", GetValue(identity, "user.id"));
Assert.Equal(false, GetValue(identity, "user.isClient"));
AssetUser(identity, "2", false, true);
}
[Fact]
@ -82,6 +81,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
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)
{
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>._))
.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);
}
@ -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>._))
.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 });
}

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.Tasks;
using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
@ -26,9 +29,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{
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 IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly DomainId parentId = DomainId.NewGuid();
private readonly DomainId assetId = DomainId.NewGuid();
private readonly AssetFile file = new NoopAssetFile();
@ -41,13 +47,42 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
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>._))
.Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
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>()));
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);
}
@ -86,6 +121,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
Slug = file.FileName.ToAssetSlug()
})
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -136,6 +174,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
Slug = file.FileName.ToAssetSlug()
})
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<create-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -163,6 +204,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
MimeType = file.MimeType
})
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -190,6 +234,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
MimeType = file.MimeType
})
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<update-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -209,6 +256,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { FileName = command.FileName })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -228,6 +278,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { Slug = command.Slug })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -247,6 +300,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { IsProtected = command.IsProtected })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -266,6 +322,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { Metadata = command.Metadata })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<anootate-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
[Fact]
@ -283,6 +342,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetAnnotated { Tags = new HashSet<string> { "tag1" } })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<annotate-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -302,6 +364,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetMoved { ParentId = parentId })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<move-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -322,6 +387,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 })
);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -332,7 +400,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id, SearchScope.All, A<CancellationToken>._))
.Returns(true);
.Returns(false);
var result = await PublishAsync(command);
@ -340,6 +408,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
Assert.Equal(EtagVersion.Empty, sut.Snapshot.Version);
Assert.Empty(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
}
[Fact]
@ -353,6 +424,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.Returns(true);
await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command));
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustNotHaveHappened();
}
[Fact]
@ -366,6 +440,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
.Returns(true);
await PublishAsync(command);
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<delete-script>", ScriptOptions(), default))
.MustHaveHappened();
}
private Task ExecuteCreateAsync()
@ -383,6 +460,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
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
{
@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.Tasks;
using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
@ -20,7 +23,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
{
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 IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly DomainId parentId = DomainId.NewGuid();
private readonly DomainId assetFolderId = DomainId.NewGuid();
private readonly AssetFolderDomainObject sut;
@ -32,10 +38,25 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
public AssetFolderDomainObjectTests()
{
app = Mocks.App(AppNamedId, Language.DE);
A.CallTo(() => appProvider.GetAppAsync(AppId, false))
.Returns(app);
A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A<CancellationToken>._))
.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);
}

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

@ -6,9 +6,11 @@
// ==========================================================================
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Assets.Commands;
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 NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly RefToken actor = RefToken.User("123");
[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>._))
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery),
ValidationAssert.Throws(() => operation.MustHaveName(null!),
new ValidationError("Folder name is required.", "FolderName"));
}
[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>._))
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery),
new ValidationError("Asset folder does not exist.", "ParentId"));
operation.MustHaveName("Folder");
}
[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>._))
.Returns(new List<IAssetFolderEntity> { AssetFolder() });
var operation = Operation(CreateAssetFolder());
await GuardAssetFolder.CanCreate(command, assetQuery);
}
[Fact]
public async Task CanCreate_should_not_throw_exception_if_added_to_root()
{
var command = new CreateAssetFolder { AppId = appId, FolderName = "My Folder" };
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._))
.Returns(new List<IAssetFolderEntity>());
await GuardAssetFolder.CanCreate(command, assetQuery);
await ValidationAssert.ThrowsAsync(() => operation.MustMoveToValidFolder(parentId),
new ValidationError("Asset folder does not exist.", "ParentId"));
}
[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>._))
.Returns(new List<IAssetFolderEntity>
{
AssetFolder(id),
AssetFolder(command.ParentId)
});
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._))
.Returns(new List<IAssetFolderEntity> { CreateAssetFolder() });
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, AssetFolder(id), assetQuery),
new ValidationError("Cannot add folder to its own child.", "ParentId"));
await operation.MustMoveToValidFolder(parentId);
}
[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>._))
.Returns(new List<IAssetFolderEntity>());
var operation = Operation(CreateAssetFolder(default, parentId));
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, AssetFolder(), assetQuery),
new ValidationError("Asset folder does not exist.", "ParentId"));
await operation.MustMoveToValidFolder(parentId);
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.MustNotHaveHappened();
}
[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>._))
.Returns(new List<IAssetFolderEntity> { AssetFolder() });
var operation = Operation(CreateAssetFolder());
await GuardAssetFolder.CanMove(command, AssetFolder(), assetQuery);
await operation.MustMoveToValidFolder(parentId);
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, A<DomainId>._, A<CancellationToken>._))
.MustNotHaveHappened();
}
[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]
public async Task CanMove_should_not_throw_exception_if_added_to_root()
{
var command = new MoveAssetFolder { AppId = appId };
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A<CancellationToken>._))
.Returns(new List<IAssetFolderEntity>
{
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]
public void CanRename_should_throw_exception_if_folder_name_is_empty()
private AssetFolderOperation Operation(IAssetFolderEntity assetFolder)
{
var command = new RenameAssetFolder { AppId = appId };
ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command),
new ValidationError("Folder name is required.", "FolderName"));
return Operation(assetFolder, Mocks.FrontendUser());
}
[Fact]
public void CanRename_should_not_throw_exception_if_names_are_different()
private AssetFolderOperation Operation(IAssetFolderEntity assetFolder, ClaimsPrincipal? currentUser)
{
var command = new RenameAssetFolder { AppId = appId, FolderName = "New Folder Name" };
GuardAssetFolder.CanRename(command);
var serviceProvider =
new ServiceCollection()
.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>();
A.CallTo(() => assetFolder.Id)
.Returns(id == default ? DomainId.NewGuid() : id);
.Returns(OrNew(id));
A.CallTo(() => assetFolder.AppId)
.Returns(appId);
A.CallTo(() => assetFolder.ParentId)
.Returns(parentId == default ? DomainId.NewGuid() : parentId);
.Returns(OrNew(parentId));
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.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Assets.Commands;
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 IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly RefToken actor = RefToken.User("123");
[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 command = new MoveAsset { AppId = appId, ParentId = parentId };
var operation = Operation(CreateAsset());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.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"));
}
[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 command = new MoveAsset { AppId = appId, ParentId = parentId, OptimizeValidation = true };
var operation = Operation(CreateAsset());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, default))
.Returns(new List<IAssetFolderEntity>());
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.Returns(new List<IAssetFolderEntity> { CreateAssetFolder() });
await GuardAsset.CanMove(command, Asset(), assetQuery);
await operation.MustMoveToValidFolder(parentId);
}
[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 command = new MoveAsset { AppId = appId, ParentId = parentId };
var operation = Operation(CreateAsset(default, parentId));
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, command.ParentId, default))
.Returns(new List<IAssetFolderEntity> { AssetFolder() });
await operation.MustMoveToValidFolder(parentId);
await GuardAsset.CanMove(command, Asset(), assetQuery);
A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default))
.MustNotHaveHappened();
}
[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]
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]
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, asset.Id, SearchScope.All, default))
A.CallTo(() => contentRepository.HasReferrersAsync(appId.Id, operation.CommandId, SearchScope.All, default))
.Returns(true);
await Assert.ThrowsAsync<DomainException>(() => GuardAsset.CanDelete(command, asset, contentRepository));
await Assert.ThrowsAsync<DomainException>(() => operation.CheckReferrersAsync());
}
[Fact]
public async Task CanDelete_should_not_throw_exception()
private AssetOperation Operation(AssetEntity asset)
{
var command = new DeleteAsset { AppId = appId };
await GuardAsset.CanDelete(command, Asset(), contentRepository);
return Operation(asset, Mocks.FrontendUser());
}
private IAssetEntity Asset(DomainId id = default, DomainId parentId = default)
private AssetOperation Operation(AssetEntity asset, ClaimsPrincipal? currentUser)
{
var asset = A.Fake<IAssetEntity>();
A.CallTo(() => asset.Id)
.Returns(id == default ? DomainId.NewGuid() : id);
A.CallTo(() => asset.AppId)
.Returns(appId);
A.CallTo(() => asset.ParentId)
.Returns(parentId == default ? DomainId.NewGuid() : parentId);
var serviceProvider =
new ServiceCollection()
.AddSingleton(contentRepository)
.AddSingleton(assetQuery)
.BuildServiceProvider();
return new AssetOperation(serviceProvider, () => asset)
{
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>();
A.CallTo(() => assetFolder.Id)
.Returns(id == default ? DomainId.NewGuid() : id);
.Returns(OrNew(id));
A.CallTo(() => assetFolder.AppId)
.Returns(appId);
A.CallTo(() => assetFolder.ParentId)
.Returns(parentId == default ? DomainId.NewGuid() : parentId);
.Returns(OrNew(parentId));
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