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