mirror of https://github.com/Squidex/squidex.git
Browse Source
* Initial commit for new feature. * Fixes. * Final fixes. * Fix merge issue. * A few last fixes. * Compiler fix.pull/768/head
committed by
GitHub
126 changed files with 3656 additions and 1212 deletions
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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'."; |
||||
|
} |
||||
|
} |
||||
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 }; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue