mirror of https://github.com/Squidex/squidex.git
Browse Source
* Docs fixed * Parent path. * Fixes. * Parent path. * Just a renaming. * API cleanup. * Some tests. * Tests * Tests fixed * Delete unused test.pull/679/head
committed by
GitHub
93 changed files with 1192 additions and 1505 deletions
@ -1,18 +1,16 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
// All rights reserved. Licensed under the MIT license.
|
// All rights reserved. Licensed under the MIT license.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Infrastructure; |
using Squidex.Infrastructure; |
||||
using Squidex.Infrastructure.Commands; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject |
namespace Squidex.Domain.Apps.Entities.Assets.Commands |
||||
{ |
{ |
||||
public interface IAssetFolderResolver |
public interface IMoveAssetCommand : IAppCommand |
||||
{ |
{ |
||||
Task<DomainId> ResolveOrCreateAsync(Context context, ICommandBus commandBus, string path); |
DomainId ParentId { get; set; } |
||||
} |
} |
||||
} |
} |
||||
@ -1,117 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Caching; |
|
||||
using Squidex.Domain.Apps.Entities.Assets.Commands; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject |
|
||||
{ |
|
||||
public sealed class AssetFolderResolver : IAssetFolderResolver |
|
||||
{ |
|
||||
private static readonly char[] TrimChars = { '/', '\\' }; |
|
||||
private static readonly char[] SplitChars = { ' ', '/', '\\' }; |
|
||||
private readonly ILocalCache localCache; |
|
||||
private readonly IAssetQueryService assetQuery; |
|
||||
|
|
||||
public AssetFolderResolver(ILocalCache localCache, IAssetQueryService assetQuery) |
|
||||
{ |
|
||||
Guard.NotNull(localCache, nameof(localCache)); |
|
||||
Guard.NotNull(assetQuery, nameof(assetQuery)); |
|
||||
|
|
||||
this.localCache = localCache; |
|
||||
this.assetQuery = assetQuery; |
|
||||
} |
|
||||
|
|
||||
public async Task<DomainId> ResolveOrCreateAsync(Context context, ICommandBus commandBus, string path) |
|
||||
{ |
|
||||
Guard.NotNull(commandBus, nameof(commandBus)); |
|
||||
Guard.NotNull(path, nameof(path)); |
|
||||
|
|
||||
path = path.Trim(TrimChars); |
|
||||
|
|
||||
var elements = path.Split(SplitChars, StringSplitOptions.RemoveEmptyEntries); |
|
||||
|
|
||||
if (elements.Length == 0) |
|
||||
{ |
|
||||
return DomainId.Empty; |
|
||||
} |
|
||||
|
|
||||
var currentId = DomainId.Empty; |
|
||||
|
|
||||
var i = elements.Length; |
|
||||
|
|
||||
for (; i > 0; i--) |
|
||||
{ |
|
||||
var subPath = string.Join('/', elements.Take(i)); |
|
||||
|
|
||||
if (localCache.TryGetValue(GetCacheKey(subPath), out var cached) && cached is DomainId id) |
|
||||
{ |
|
||||
currentId = id; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
var creating = false; |
|
||||
|
|
||||
for (; i < elements.Length; i++) |
|
||||
{ |
|
||||
var name = elements[i]; |
|
||||
|
|
||||
var isResolved = false; |
|
||||
|
|
||||
if (!creating) |
|
||||
{ |
|
||||
var children = await assetQuery.QueryAssetFoldersAsync(context, currentId); |
|
||||
|
|
||||
foreach (var child in children) |
|
||||
{ |
|
||||
var childPath = string.Join('/', elements.Take(i).Union(Enumerable.Repeat(child.FolderName, 1))); |
|
||||
|
|
||||
localCache.Add(GetCacheKey(childPath), child.Id); |
|
||||
} |
|
||||
|
|
||||
foreach (var child in children) |
|
||||
{ |
|
||||
if (child.FolderName == name) |
|
||||
{ |
|
||||
currentId = child.Id; |
|
||||
|
|
||||
isResolved = true; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (!isResolved) |
|
||||
{ |
|
||||
var command = new CreateAssetFolder { ParentId = currentId, FolderName = name }; |
|
||||
|
|
||||
await commandBus.PublishAsync(command); |
|
||||
|
|
||||
currentId = command.AssetFolderId; |
|
||||
creating = true; |
|
||||
} |
|
||||
|
|
||||
var newPath = string.Join('/', elements.Take(i).Union(Enumerable.Repeat(name, 1))); |
|
||||
|
|
||||
localCache.Add(GetCacheKey(newPath), currentId); |
|
||||
} |
|
||||
|
|
||||
return currentId; |
|
||||
} |
|
||||
|
|
||||
private static object GetCacheKey(string path) |
|
||||
{ |
|
||||
return $"ASSET_FOLDERS_{path}"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,235 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Domain.Apps.Core; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Core.DefaultValues; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Domain.Apps.Core.Scripting; |
|
||||
using Squidex.Domain.Apps.Core.ValidateContent; |
|
||||
using Squidex.Domain.Apps.Entities.Apps; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Json; |
|
||||
using Squidex.Infrastructure.Validation; |
|
||||
using Squidex.Log; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject |
|
||||
{ |
|
||||
public sealed class ContentOperationContext |
|
||||
{ |
|
||||
private static readonly ScriptOptions ScriptOptions = new ScriptOptions |
|
||||
{ |
|
||||
AsContext = true, |
|
||||
CanDisallow = true, |
|
||||
CanReject = true |
|
||||
}; |
|
||||
|
|
||||
private readonly IScriptEngine scriptEngine; |
|
||||
private readonly ISemanticLog log; |
|
||||
private readonly IAppProvider appProvider; |
|
||||
private readonly IEnumerable<IValidatorsFactory> validators; |
|
||||
private readonly IContentWorkflow contentWorkflow; |
|
||||
private readonly IContentRepository contentRepository; |
|
||||
private readonly IJsonSerializer jsonSerializer; |
|
||||
private ISchemaEntity schema; |
|
||||
private IAppEntity app; |
|
||||
private ContentCommand command; |
|
||||
private ValidationContext validationContext; |
|
||||
|
|
||||
public IContentWorkflow Workflow => contentWorkflow; |
|
||||
|
|
||||
public IContentRepository Repository => contentRepository; |
|
||||
|
|
||||
public ContentOperationContext( |
|
||||
IAppProvider appProvider, |
|
||||
IEnumerable<IValidatorsFactory> validators, |
|
||||
IContentWorkflow contentWorkflow, |
|
||||
IContentRepository contentRepository, |
|
||||
IJsonSerializer jsonSerializer, |
|
||||
IScriptEngine scriptEngine, |
|
||||
ISemanticLog log) |
|
||||
{ |
|
||||
Guard.NotDefault(appProvider, nameof(appProvider)); |
|
||||
Guard.NotDefault(validators, nameof(validators)); |
|
||||
Guard.NotDefault(contentWorkflow, nameof(contentWorkflow)); |
|
||||
Guard.NotDefault(contentRepository, nameof(contentRepository)); |
|
||||
Guard.NotDefault(jsonSerializer, nameof(jsonSerializer)); |
|
||||
Guard.NotDefault(scriptEngine, nameof(scriptEngine)); |
|
||||
Guard.NotDefault(log, nameof(log)); |
|
||||
|
|
||||
this.appProvider = appProvider; |
|
||||
this.validators = validators; |
|
||||
this.contentWorkflow = contentWorkflow; |
|
||||
this.contentRepository = contentRepository; |
|
||||
this.jsonSerializer = jsonSerializer; |
|
||||
this.scriptEngine = scriptEngine; |
|
||||
|
|
||||
this.log = log; |
|
||||
} |
|
||||
|
|
||||
public ISchemaEntity Schema |
|
||||
{ |
|
||||
get => schema; |
|
||||
} |
|
||||
|
|
||||
public async Task LoadAsync(NamedId<DomainId> appId, NamedId<DomainId> schemaId, ContentCommand command, bool optimized) |
|
||||
{ |
|
||||
this.command = command; |
|
||||
|
|
||||
var (app, schema) = await appProvider.GetAppWithSchemaAsync(appId.Id, schemaId.Id); |
|
||||
|
|
||||
if (app == null) |
|
||||
{ |
|
||||
throw new DomainObjectNotFoundException(appId.ToString()); |
|
||||
} |
|
||||
|
|
||||
this.app = app; |
|
||||
|
|
||||
if (schema == null) |
|
||||
{ |
|
||||
throw new DomainObjectNotFoundException(schemaId.ToString()); |
|
||||
} |
|
||||
|
|
||||
this.schema = schema; |
|
||||
|
|
||||
validationContext = new ValidationContext(jsonSerializer, appId, schemaId, schema.SchemaDef, command.ContentId).Optimized(optimized); |
|
||||
} |
|
||||
|
|
||||
public Task<Status> GetInitialStatusAsync() |
|
||||
{ |
|
||||
return contentWorkflow.GetInitialStatusAsync(schema); |
|
||||
} |
|
||||
|
|
||||
public Task GenerateDefaultValuesAsync(ContentData data) |
|
||||
{ |
|
||||
data.GenerateDefaultValues(schema.SchemaDef, Partition()); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
public async Task ValidateInputAsync(ContentData data) |
|
||||
{ |
|
||||
var validator = |
|
||||
new ContentValidator(Partition(), |
|
||||
validationContext, validators, log); |
|
||||
|
|
||||
await validator.ValidateInputAsync(data); |
|
||||
|
|
||||
CheckErrors(validator); |
|
||||
} |
|
||||
|
|
||||
public async Task ValidateInputPartialAsync(ContentData data) |
|
||||
{ |
|
||||
var validator = |
|
||||
new ContentValidator(Partition(), |
|
||||
validationContext, validators, log); |
|
||||
|
|
||||
await validator.ValidateInputPartialAsync(data); |
|
||||
|
|
||||
CheckErrors(validator); |
|
||||
} |
|
||||
|
|
||||
public async Task ValidateContentAsync(ContentData data) |
|
||||
{ |
|
||||
var validator = |
|
||||
new ContentValidator(Partition(), |
|
||||
validationContext, validators, log); |
|
||||
|
|
||||
await validator.ValidateContentAsync(data); |
|
||||
|
|
||||
CheckErrors(validator); |
|
||||
} |
|
||||
|
|
||||
public async Task ValidateContentAndInputAsync(ContentData data) |
|
||||
{ |
|
||||
var validator = |
|
||||
new ContentValidator(Partition(), |
|
||||
validationContext.AsPublishing(), validators, log); |
|
||||
|
|
||||
await validator.ValidateInputAsync(data); |
|
||||
await validator.ValidateContentAsync(data); |
|
||||
|
|
||||
CheckErrors(validator); |
|
||||
} |
|
||||
|
|
||||
public Task ValidateOnPublishAsync(ContentData data) |
|
||||
{ |
|
||||
if (!schema.SchemaDef.Properties.ValidateOnPublish) |
|
||||
{ |
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
return ValidateContentAndInputAsync(data); |
|
||||
} |
|
||||
|
|
||||
private static void CheckErrors(ContentValidator validator) |
|
||||
{ |
|
||||
if (validator.Errors.Count > 0) |
|
||||
{ |
|
||||
throw new ValidationException(validator.Errors.ToList()); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public bool HasScript(Func<SchemaScripts, string> script) |
|
||||
{ |
|
||||
return !string.IsNullOrWhiteSpace(GetScript(script)); |
|
||||
} |
|
||||
|
|
||||
public async Task<ContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptVars context) |
|
||||
{ |
|
||||
Enrich(context); |
|
||||
|
|
||||
var actualScript = GetScript(script); |
|
||||
|
|
||||
if (string.IsNullOrWhiteSpace(actualScript)) |
|
||||
{ |
|
||||
return context.Data!; |
|
||||
} |
|
||||
|
|
||||
return await scriptEngine.TransformAsync(context, actualScript, ScriptOptions); |
|
||||
} |
|
||||
|
|
||||
public async Task ExecuteScriptAsync(Func<SchemaScripts, string> script, ScriptVars context) |
|
||||
{ |
|
||||
Enrich(context); |
|
||||
|
|
||||
var actualScript = GetScript(script); |
|
||||
|
|
||||
if (string.IsNullOrWhiteSpace(actualScript)) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
await scriptEngine.ExecuteAsync(context, GetScript(script), ScriptOptions); |
|
||||
} |
|
||||
|
|
||||
private PartitionResolver Partition() |
|
||||
{ |
|
||||
return app.PartitionResolver(); |
|
||||
} |
|
||||
|
|
||||
private void Enrich(ScriptVars context) |
|
||||
{ |
|
||||
context.ContentId = command.ContentId; |
|
||||
context.AppId = app.Id; |
|
||||
context.AppName = app.Name; |
|
||||
context.User = command.User; |
|
||||
} |
|
||||
|
|
||||
private string GetScript(Func<SchemaScripts, string> script) |
|
||||
{ |
|
||||
return script(schema.SchemaDef.Scripts); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,201 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Translations; |
|
||||
using Squidex.Infrastructure.Validation; |
|
||||
using Squidex.Shared; |
|
||||
using Squidex.Shared.Identity; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards |
|
||||
{ |
|
||||
public static class GuardContent |
|
||||
{ |
|
||||
public static void CanCreate(CreateContent command, ISchemaEntity schema) |
|
||||
{ |
|
||||
Guard.NotNull(command, nameof(command)); |
|
||||
|
|
||||
if (schema.SchemaDef.IsSingleton) |
|
||||
{ |
|
||||
if (command.ContentId != schema.Id) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.singletonNotCreatable")); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Validate.It(e => |
|
||||
{ |
|
||||
if (command.Data == null) |
|
||||
{ |
|
||||
e(Not.Defined(nameof(command.Data)), nameof(command.Data)); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public static async Task CanUpdate(UpdateContent command, IContentEntity content, IContentWorkflow contentWorkflow) |
|
||||
{ |
|
||||
Guard.NotNull(command, nameof(command)); |
|
||||
|
|
||||
CheckPermission(content, command, Permissions.AppContentsUpdate, Permissions.AppContentsUpsert); |
|
||||
|
|
||||
Validate.It(e => |
|
||||
{ |
|
||||
if (command.Data == null) |
|
||||
{ |
|
||||
e(Not.Defined(nameof(command.Data)), nameof(command.Data)); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
if (!command.DoNotValidateWorkflow) |
|
||||
{ |
|
||||
var status = content.NewStatus ?? content.Status; |
|
||||
|
|
||||
if (!await contentWorkflow.CanUpdateAsync(content, status, command.User)) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static void CanDeleteDraft(DeleteContentDraft command, IContentEntity content) |
|
||||
{ |
|
||||
Guard.NotNull(command, nameof(command)); |
|
||||
|
|
||||
CheckPermission(content, command, Permissions.AppContentsVersionDelete); |
|
||||
|
|
||||
if (content.NewStatus == null) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.draftToDeleteNotFound")); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static void CanCreateDraft(CreateContentDraft command, IContentEntity content) |
|
||||
{ |
|
||||
Guard.NotNull(command, nameof(command)); |
|
||||
|
|
||||
CheckPermission(content, command, Permissions.AppContentsVersionCreate); |
|
||||
|
|
||||
if (content.Status != Status.Published) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.draftNotCreateForUnpublished")); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static async Task CanChangeStatus(ChangeContentStatus command, |
|
||||
IContentEntity content, |
|
||||
IContentWorkflow contentWorkflow, |
|
||||
IContentRepository contentRepository, |
|
||||
ISchemaEntity schema) |
|
||||
{ |
|
||||
Guard.NotNull(command, nameof(command)); |
|
||||
|
|
||||
CheckPermission(content, command, Permissions.AppContentsChangeStatus, Permissions.AppContentsUpsert); |
|
||||
|
|
||||
var newStatus = command.Status; |
|
||||
|
|
||||
if (schema.SchemaDef.IsSingleton) |
|
||||
{ |
|
||||
if (content.NewStatus == null || newStatus != Status.Published) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.singletonNotChangeable")); |
|
||||
} |
|
||||
|
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
var oldStatus = content.NewStatus ?? content.Status; |
|
||||
|
|
||||
if (command.Status == oldStatus) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (oldStatus == Status.Published && command.CheckReferrers) |
|
||||
{ |
|
||||
var hasReferrer = await contentRepository.HasReferrersAsync(content.AppId.Id, command.ContentId, SearchScope.Published); |
|
||||
|
|
||||
if (hasReferrer) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.referenced")); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
await Validate.It(async e => |
|
||||
{ |
|
||||
if (!command.DoNotValidateWorkflow) |
|
||||
{ |
|
||||
if (!await contentWorkflow.CanMoveToAsync(content, oldStatus, newStatus, command.User)) |
|
||||
{ |
|
||||
var values = new { oldStatus, newStatus }; |
|
||||
|
|
||||
e(T.Get("contents.statusTransitionNotAllowed", values), "Status"); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
var info = await contentWorkflow.GetInfoAsync(content, newStatus); |
|
||||
|
|
||||
if (info == null) |
|
||||
{ |
|
||||
e(T.Get("contents.statusNotValid"), "Status"); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public static async Task CanDelete(DeleteContent command, |
|
||||
IContentEntity content, |
|
||||
IContentRepository contentRepository, |
|
||||
ISchemaEntity schema) |
|
||||
{ |
|
||||
Guard.NotNull(command, nameof(command)); |
|
||||
|
|
||||
CheckPermission(content, command, Permissions.AppContentsDeleteOwn); |
|
||||
|
|
||||
if (schema.SchemaDef.IsSingleton) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.singletonNotDeletable")); |
|
||||
} |
|
||||
|
|
||||
if (command.CheckReferrers) |
|
||||
{ |
|
||||
var hasReferrer = await contentRepository.HasReferrersAsync(content.AppId.Id, content.Id, SearchScope.All); |
|
||||
|
|
||||
if (hasReferrer) |
|
||||
{ |
|
||||
throw new DomainException(T.Get("contents.referenced")); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static void CanValidate(ValidateContent command, IContentEntity content) |
|
||||
{ |
|
||||
Guard.NotNull(command, nameof(command)); |
|
||||
|
|
||||
CheckPermission(content, command, Permissions.AppContentsRead); |
|
||||
} |
|
||||
|
|
||||
public static void CheckPermission(IContentEntity content, ContentCommand command, params string[] permissions) |
|
||||
{ |
|
||||
if (Equals(content.CreatedBy, command.Actor) || command.User == null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (permissions.All(x => !command.User.Allows(x, content.AppId.Name, content.SchemaId.Name))) |
|
||||
{ |
|
||||
throw new DomainForbiddenException(T.Get("common.errorNoPermission")); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,122 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Scripting; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards |
||||
|
{ |
||||
|
public static class ScriptingExtensions |
||||
|
{ |
||||
|
private static readonly ScriptOptions Options = new ScriptOptions |
||||
|
{ |
||||
|
AsContext = true, |
||||
|
CanDisallow = true, |
||||
|
CanReject = true |
||||
|
}; |
||||
|
|
||||
|
public static async Task<ContentData> ExecuteCreateScriptAsync(this OperationContext context, ContentData data, Status status) |
||||
|
{ |
||||
|
var script = context.SchemaDef.Scripts.Create; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(script)) |
||||
|
{ |
||||
|
var vars = Enrich(context, new ScriptVars |
||||
|
{ |
||||
|
Operation = "Create", |
||||
|
Data = data, |
||||
|
DataOld = default, |
||||
|
Status = status, |
||||
|
StatusOld = default |
||||
|
}); |
||||
|
|
||||
|
data = await GetScriptEngine(context).TransformAsync(vars, script, Options); |
||||
|
} |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
public static async Task<ContentData> ExecuteUpdateScriptAsync(this OperationContext context, ContentData data) |
||||
|
{ |
||||
|
var script = context.SchemaDef.Scripts.Update; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(script)) |
||||
|
{ |
||||
|
var vars = Enrich(context, new ScriptVars |
||||
|
{ |
||||
|
Operation = "Update", |
||||
|
Data = data, |
||||
|
DataOld = context.Content.Data, |
||||
|
Status = context.Content.EditingStatus(), |
||||
|
StatusOld = default |
||||
|
}); |
||||
|
|
||||
|
data = await GetScriptEngine(context).TransformAsync(vars, script, Options); |
||||
|
} |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
public static async Task<ContentData> ExecuteChangeScriptAsync(this OperationContext context, Status status, StatusChange change) |
||||
|
{ |
||||
|
var script = context.SchemaDef.Scripts.Change; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(script)) |
||||
|
{ |
||||
|
var data = context.Content.Data.Clone(); |
||||
|
|
||||
|
var vars = Enrich(context, new ScriptVars |
||||
|
{ |
||||
|
Operation = change.ToString(), |
||||
|
Data = data, |
||||
|
DataOld = default, |
||||
|
Status = status, |
||||
|
StatusOld = context.Content.EditingStatus() |
||||
|
}); |
||||
|
|
||||
|
return await GetScriptEngine(context).TransformAsync(vars, script, Options); |
||||
|
} |
||||
|
|
||||
|
return context.Content.Data; |
||||
|
} |
||||
|
|
||||
|
public static async Task ExecuteDeleteScriptAsync(this OperationContext context) |
||||
|
{ |
||||
|
var script = context.SchemaDef.Scripts.Delete; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(script)) |
||||
|
{ |
||||
|
var vars = Enrich(context, new ScriptVars |
||||
|
{ |
||||
|
Operation = "Delete", |
||||
|
Data = context.Content.Data, |
||||
|
DataOld = default, |
||||
|
Status = context.Content.EditingStatus(), |
||||
|
StatusOld = default |
||||
|
}); |
||||
|
|
||||
|
await GetScriptEngine(context).ExecuteAsync(vars, script, Options); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static IScriptEngine GetScriptEngine(OperationContext context) |
||||
|
{ |
||||
|
return context.Resolve<IScriptEngine>(); |
||||
|
} |
||||
|
|
||||
|
private static ScriptVars Enrich(OperationContext context, ScriptVars vars) |
||||
|
{ |
||||
|
vars.ContentId = context.ContentId; |
||||
|
vars.AppId = context.App.Id; |
||||
|
vars.AppName = context.App.Name; |
||||
|
vars.User = context.User; |
||||
|
|
||||
|
return vars; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
using Squidex.Shared.Identity; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards |
||||
|
{ |
||||
|
public static class SecurityExtensions |
||||
|
{ |
||||
|
public static void MustHavePermission(this OperationContext context, string permissionId) |
||||
|
{ |
||||
|
var content = context.Content; |
||||
|
|
||||
|
if (Equals(content.CreatedBy, context.Actor) || context.User == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!context.User.Allows(permissionId, content.AppId.Name, content.SchemaId.Name)) |
||||
|
{ |
||||
|
throw new DomainForbiddenException(T.Get("common.errorNoPermission")); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards |
||||
|
{ |
||||
|
public static class SingletonExtensions |
||||
|
{ |
||||
|
public static void MustNotCreateSingleton(this OperationContext context) |
||||
|
{ |
||||
|
if (context.SchemaDef.IsSingleton && context.ContentId != context.Schema.Id) |
||||
|
{ |
||||
|
throw new DomainException(T.Get("contents.singletonNotCreatable")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void MustNotChangeSingleton(this OperationContext context, Status status) |
||||
|
{ |
||||
|
if (context.SchemaDef.IsSingleton && (context.Content.NewStatus == null || status != Status.Published)) |
||||
|
{ |
||||
|
throw new DomainException(T.Get("contents.singletonNotChangeable")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void MustNotDeleteSingleton(this OperationContext context) |
||||
|
{ |
||||
|
if (context.SchemaDef.IsSingleton) |
||||
|
{ |
||||
|
throw new DomainException(T.Get("contents.singletonNotDeletable")); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.DefaultValues; |
||||
|
using Squidex.Domain.Apps.Core.ValidateContent; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
using Squidex.Infrastructure.Validation; |
||||
|
using Squidex.Log; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards |
||||
|
{ |
||||
|
public static class ValidationExtensions |
||||
|
{ |
||||
|
public static void MustDeleteDraft(this OperationContext context) |
||||
|
{ |
||||
|
if (context.Content.NewStatus == null) |
||||
|
{ |
||||
|
throw new DomainException(T.Get("contents.draftToDeleteNotFound")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void MustCreateDraft(this OperationContext context) |
||||
|
{ |
||||
|
if (context.Content.EditingStatus() != Status.Published) |
||||
|
{ |
||||
|
throw new DomainException(T.Get("contents.draftNotCreateForUnpublished")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void MustHaveData(this OperationContext context, ContentData? data) |
||||
|
{ |
||||
|
if (data == null) |
||||
|
{ |
||||
|
context.AddError(Not.Defined(nameof(data)), nameof(data)).ThrowOnErrors(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async Task ValidateInputAsync(this OperationContext context, ContentData data, bool optimize) |
||||
|
{ |
||||
|
var validator = GetValidator(context, optimize); |
||||
|
|
||||
|
await validator.ValidateInputAsync(data); |
||||
|
|
||||
|
context.AddErrors(validator.Errors).ThrowOnErrors(); |
||||
|
} |
||||
|
|
||||
|
public static async Task ValidateInputPartialAsync(this OperationContext context, ContentData data, bool optimize) |
||||
|
{ |
||||
|
var validator = GetValidator(context, optimize); |
||||
|
|
||||
|
await validator.ValidateInputPartialAsync(data); |
||||
|
|
||||
|
context.AddErrors(validator.Errors).ThrowOnErrors(); |
||||
|
} |
||||
|
|
||||
|
public static async Task ValidateContentAsync(this OperationContext context, ContentData data, bool optimize) |
||||
|
{ |
||||
|
var validator = GetValidator(context, optimize); |
||||
|
|
||||
|
await validator.ValidateContentAsync(data); |
||||
|
|
||||
|
context.AddErrors(validator.Errors).ThrowOnErrors(); |
||||
|
} |
||||
|
|
||||
|
public static async Task ValidateContentAndInputAsync(this OperationContext operation, ContentData data, bool optimize) |
||||
|
{ |
||||
|
var validator = GetValidator(operation, optimize); |
||||
|
|
||||
|
await validator.ValidateInputAsync(data); |
||||
|
await validator.ValidateContentAsync(data); |
||||
|
|
||||
|
operation.AddErrors(validator.Errors).ThrowOnErrors(); |
||||
|
} |
||||
|
|
||||
|
public static void GenerateDefaultValues(this OperationContext context, ContentData data) |
||||
|
{ |
||||
|
data.GenerateDefaultValues(context.Schema.SchemaDef, context.Partition()); |
||||
|
} |
||||
|
|
||||
|
public static async Task CheckReferrersAsync(this OperationContext context) |
||||
|
{ |
||||
|
var contentRepository = context.Resolve<IContentRepository>(); |
||||
|
|
||||
|
var hasReferrer = await contentRepository.HasReferrersAsync(context.App.Id, context.ContentId, SearchScope.All); |
||||
|
|
||||
|
if (hasReferrer) |
||||
|
{ |
||||
|
throw new DomainException(T.Get("contents.referenced")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static ContentValidator GetValidator(this OperationContext context, bool optimize) |
||||
|
{ |
||||
|
var validationContext = |
||||
|
new ValidationContext(context.Resolve<IJsonSerializer>(), |
||||
|
context.App.NamedId(), |
||||
|
context.Schema.NamedId(), |
||||
|
context.SchemaDef, |
||||
|
context.ContentId) |
||||
|
.Optimized(optimize); |
||||
|
|
||||
|
var validator = |
||||
|
new ContentValidator(context.Partition(), |
||||
|
validationContext, |
||||
|
context.Resolve<IEnumerable<IValidatorsFactory>>(), |
||||
|
context.Resolve<ISemanticLog>()); |
||||
|
|
||||
|
return validator; |
||||
|
} |
||||
|
|
||||
|
private static PartitionResolver Partition(this OperationContext context) |
||||
|
{ |
||||
|
return context.App.PartitionResolver(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards |
||||
|
{ |
||||
|
public static class WorkflowExtensions |
||||
|
{ |
||||
|
public static Task<Status> GetInitialStatusAsync(this OperationContext context) |
||||
|
{ |
||||
|
var workflow = GetWorkflow(context); |
||||
|
|
||||
|
return workflow.GetInitialStatusAsync(context.Schema); |
||||
|
} |
||||
|
|
||||
|
public static async Task CheckTransitionAsync(this OperationContext context, Status status) |
||||
|
{ |
||||
|
if (!context.SchemaDef.IsSingleton) |
||||
|
{ |
||||
|
var workflow = GetWorkflow(context); |
||||
|
|
||||
|
var oldStatus = context.Content.EditingStatus(); |
||||
|
|
||||
|
if (!await workflow.CanMoveToAsync(context.Content, oldStatus, status, context.User)) |
||||
|
{ |
||||
|
var values = new { oldStatus, newStatus = status }; |
||||
|
|
||||
|
context.AddError(T.Get("contents.statusTransitionNotAllowed", values), nameof(status)); |
||||
|
context.ThrowOnErrors(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async Task CheckStatusAsync(this OperationContext context, Status status) |
||||
|
{ |
||||
|
if (!context.SchemaDef.IsSingleton) |
||||
|
{ |
||||
|
var workflow = GetWorkflow(context); |
||||
|
|
||||
|
var statusInfo = await workflow.GetInfoAsync(context.Content, status); |
||||
|
|
||||
|
if (statusInfo == null) |
||||
|
{ |
||||
|
context.AddError(T.Get("contents.statusNotValid"), nameof(status)); |
||||
|
context.ThrowOnErrors(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async Task CheckUpdateAsync(this OperationContext context) |
||||
|
{ |
||||
|
if (context.User != null) |
||||
|
{ |
||||
|
var workflow = GetWorkflow(context); |
||||
|
|
||||
|
var status = context.Content.EditingStatus(); |
||||
|
|
||||
|
if (!await workflow.CanUpdateAsync(context.Content, status, context.User)) |
||||
|
{ |
||||
|
throw new DomainException(T.Get("contents.workflowErrorUpdate", new { status })); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static IContentWorkflow GetWorkflow(OperationContext context) |
||||
|
{ |
||||
|
return context.Resolve<IContentWorkflow>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 GraphQL.Utilities; |
||||
|
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 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()); |
||||
|
} |
||||
|
|
||||
|
return new OperationContext(services) |
||||
|
{ |
||||
|
App = app, |
||||
|
Actor = command.Actor, |
||||
|
ContentProvider = snapshot, |
||||
|
ContentId = command.ContentId, |
||||
|
Schema = schema, |
||||
|
User = command.User |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public T Resolve<T>() |
||||
|
{ |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,165 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using FakeItEasy; |
|
||||
using Squidex.Caching; |
|
||||
using Squidex.Domain.Apps.Entities.Assets.Commands; |
|
||||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject |
|
||||
{ |
|
||||
public class AssetFolderResolverTests |
|
||||
{ |
|
||||
private readonly ILocalCache localCache = new AsyncLocalCache(); |
|
||||
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); |
|
||||
private readonly ICommandBus commandBus = A.Fake<ICommandBus>(); |
|
||||
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); |
|
||||
private readonly Context requestContext; |
|
||||
private readonly AssetFolderResolver sut; |
|
||||
|
|
||||
public AssetFolderResolverTests() |
|
||||
{ |
|
||||
requestContext = Context.Anonymous(Mocks.App(appId)); |
|
||||
|
|
||||
localCache.StartContext(); |
|
||||
|
|
||||
sut = new AssetFolderResolver(localCache, assetQuery); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("")] |
|
||||
[InlineData(" ")] |
|
||||
[InlineData("/")] |
|
||||
[InlineData("\\")] |
|
||||
public async Task Should_resolve_root_id_for_empty_path(string path) |
|
||||
{ |
|
||||
var folderId = await sut.ResolveOrCreateAsync(requestContext, commandBus, path); |
|
||||
|
|
||||
Assert.Equal(DomainId.Empty, folderId); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_create_and_cache_level1_folder() |
|
||||
{ |
|
||||
var folderId11_1 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1"); |
|
||||
var folderId11_2 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1"); |
|
||||
|
|
||||
Assert.NotEqual(DomainId.Empty, folderId11_1); |
|
||||
Assert.NotEqual(DomainId.Empty, folderId11_2); |
|
||||
|
|
||||
Assert.Equal(folderId11_2, folderId11_1); |
|
||||
|
|
||||
A.CallTo(() => commandBus.PublishAsync(A<CreateAssetFolder>.That.Matches(x => x.FolderName == "level1"))) |
|
||||
.MustHaveHappenedOnceExactly(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_create_and_cache_recursively() |
|
||||
{ |
|
||||
var folderId21_1 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); |
|
||||
var folderId21_2 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); |
|
||||
|
|
||||
Assert.NotEqual(DomainId.Empty, folderId21_1); |
|
||||
Assert.NotEqual(DomainId.Empty, folderId21_2); |
|
||||
|
|
||||
Assert.Equal(folderId21_1, folderId21_2); |
|
||||
|
|
||||
A.CallTo(() => commandBus.PublishAsync(A<CreateAssetFolder>.That.Matches(x => x.FolderName == "level1"))) |
|
||||
.MustHaveHappenedOnceExactly(); |
|
||||
|
|
||||
A.CallTo(() => commandBus.PublishAsync(A<CreateAssetFolder>.That.Matches(x => x.FolderName == "level2"))) |
|
||||
.MustHaveHappenedOnceExactly(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_cache_folders_on_same_level() |
|
||||
{ |
|
||||
var folder11 = CreateFolder("level1_1"); |
|
||||
var folder12 = CreateFolder("level1_2"); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) |
|
||||
.Returns(ResultList.CreateFrom(2, folder11, folder12)); |
|
||||
|
|
||||
var folderId11 = await sut.ResolveOrCreateAsync(requestContext, commandBus, folder11.FolderName); |
|
||||
var folderId12 = await sut.ResolveOrCreateAsync(requestContext, commandBus, folder12.FolderName); |
|
||||
|
|
||||
Assert.Equal(folder11.Id, folderId11); |
|
||||
Assert.Equal(folder12.Id, folderId12); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, A<DomainId>._)) |
|
||||
.MustHaveHappenedOnceExactly(); |
|
||||
|
|
||||
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._)) |
|
||||
.MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_resolve_recursively() |
|
||||
{ |
|
||||
var folder11 = CreateFolder("level1"); |
|
||||
var folder21 = CreateFolder("level2"); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) |
|
||||
.Returns(ResultList.CreateFrom(1, folder11)); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, folder11.Id)) |
|
||||
.Returns(ResultList.CreateFrom(1, folder21)); |
|
||||
|
|
||||
var folderId2 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); |
|
||||
|
|
||||
Assert.Equal(folder21.Id, folderId2); |
|
||||
|
|
||||
A.CallTo(() => commandBus.PublishAsync(A<ICommand>._)) |
|
||||
.MustNotHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_resolve_recursively_and_create_folder() |
|
||||
{ |
|
||||
var folder11 = CreateFolder("level1"); |
|
||||
var folder21 = CreateFolder("level2"); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) |
|
||||
.Returns(ResultList.CreateFrom(1, folder11)); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, folder11.Id)) |
|
||||
.Returns(ResultList.CreateFrom(1, folder21)); |
|
||||
|
|
||||
await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2"); |
|
||||
|
|
||||
var folderId3 = await sut.ResolveOrCreateAsync(requestContext, commandBus, "level1/level2/level3"); |
|
||||
|
|
||||
Assert.NotEqual(DomainId.Empty, folderId3); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, DomainId.Empty)) |
|
||||
.MustHaveHappenedOnceExactly(); |
|
||||
|
|
||||
A.CallTo(() => assetQuery.QueryAssetFoldersAsync(requestContext, folder11.Id)) |
|
||||
.MustHaveHappenedOnceExactly(); |
|
||||
|
|
||||
A.CallTo(() => commandBus.PublishAsync(A<CreateAssetFolder>.That.Matches(x => x.FolderName == "level3" && x.ParentId == folder21.Id))) |
|
||||
.MustHaveHappenedOnceExactly(); |
|
||||
} |
|
||||
|
|
||||
private static IAssetFolderEntity CreateFolder(string name) |
|
||||
{ |
|
||||
var assetFolder = A.Fake<IAssetFolderEntity>(); |
|
||||
|
|
||||
A.CallTo(() => assetFolder.FolderName) |
|
||||
.Returns(name); |
|
||||
|
|
||||
A.CallTo(() => assetFolder.Id) |
|
||||
.Returns(DomainId.NewGuid()); |
|
||||
|
|
||||
return assetFolder; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,5 @@ |
|||||
|
<sqx-dropdown [formControl]="control" [items]="snapshot.assetFolders" valueProperty="id" searchProperty="folderName"> |
||||
|
<ng-template let-assetFolder="$implicit" let-context="context"> |
||||
|
<span class="truncate">{{assetFolder.folderName | sqxTranslate | sqxHighlight:context}}</span> |
||||
|
</ng-template> |
||||
|
</sqx-dropdown> |
||||
@ -0,0 +1,76 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnInit } from '@angular/core'; |
||||
|
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { MathHelper, StatefulControlComponent, value$ } from '@app/framework'; |
||||
|
import { AssetPathItem, AssetsService } from '@app/shared/internal'; |
||||
|
import { AppsState } from '@app/shared/state/apps.state'; |
||||
|
import { ROOT_ITEM } from '@app/shared/state/assets.state'; |
||||
|
|
||||
|
export const SQX_ASSETS_FOLDER_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { |
||||
|
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AssetFolderDropdownComponent), multi: true |
||||
|
}; |
||||
|
|
||||
|
interface State { |
||||
|
// The asset folders.
|
||||
|
assetFolders: ReadonlyArray<AssetPathItem>; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-asset-folder-dropdown', |
||||
|
styleUrls: ['./asset-folder-dropdown.component.scss'], |
||||
|
templateUrl: './asset-folder-dropdown.component.html', |
||||
|
providers: [ |
||||
|
SQX_ASSETS_FOLDER_DROPDOWN_CONTROL_VALUE_ACCESSOR |
||||
|
], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush |
||||
|
}) |
||||
|
export class AssetFolderDropdownComponent extends StatefulControlComponent<State, any> implements OnInit, ControlValueAccessor { |
||||
|
public control = new FormControl(); |
||||
|
|
||||
|
constructor(changeDetector: ChangeDetectorRef, |
||||
|
private readonly appsState: AppsState, |
||||
|
private readonly assetsService: AssetsService |
||||
|
) { |
||||
|
super(changeDetector, { |
||||
|
assetFolders: [] |
||||
|
}); |
||||
|
|
||||
|
this.own( |
||||
|
value$(this.control) |
||||
|
.subscribe((value: any) => { |
||||
|
if (this.control.enabled) { |
||||
|
this.callChange(value); |
||||
|
this.callTouched(); |
||||
|
} |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.assetsService.getAssetFolders(this.appsState.appName, MathHelper.EMPTY_GUID) |
||||
|
.subscribe(dto => { |
||||
|
const assetFolders = [ROOT_ITEM, ...dto.items]; |
||||
|
|
||||
|
this.next({ assetFolders }); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public setDisabledState(isDisabled: boolean) { |
||||
|
super.setDisabledState(isDisabled); |
||||
|
|
||||
|
if (isDisabled) { |
||||
|
this.control.disable({ emitEvent: false }); |
||||
|
} else { |
||||
|
this.control.enable({ emitEvent: false }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public writeValue(obj: any): void { |
||||
|
this.control.setValue(obj || ROOT_ITEM.id); |
||||
|
} |
||||
|
} |
||||
@ -1,3 +1,3 @@ |
|||||
<sqx-checkbox-group [formControl]="selectionControl" |
<sqx-checkbox-group [formControl]="control" |
||||
[values]="snapshot.converter.suggestions"> |
[values]="snapshot.converter.suggestions"> |
||||
</sqx-checkbox-group> |
</sqx-checkbox-group> |
||||
@ -1,4 +1,4 @@ |
|||||
<sqx-dropdown [formControl]="selectionControl" [items]="snapshot.contentNames"> |
<sqx-dropdown [formControl]="control" [items]="snapshot.contentNames"> |
||||
<ng-template let-content="$implicit" let-context="context"> |
<ng-template let-content="$implicit" let-context="context"> |
||||
<span class="truncate" [innerHTML]="content.name | sqxHighlight:context"></span> |
<span class="truncate" [innerHTML]="content.name | sqxHighlight:context"></span> |
||||
</ng-template> |
</ng-template> |
||||
|
|||||
@ -1,2 +1,3 @@ |
|||||
<sqx-tag-editor placeholder="{{ 'common.tagAddReference' | sqxTranslate }}" [converter]="snapshot.converter" [formControl]="selectionControl" [suggestions]="snapshot.converter.suggestions"> |
<sqx-tag-editor placeholder="{{ 'common.tagAddReference' | sqxTranslate }}" |
||||
|
[converter]="snapshot.converter" [formControl]="control" [suggestions]="snapshot.converter.suggestions"> |
||||
</sqx-tag-editor> |
</sqx-tag-editor> |
||||
Loading…
Reference in new issue