diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs index 9e7ff611d..9c8923bda 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Core.ValidateContent string FileName { get; } + string FileHash { get; } + string Slug { get; } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index 8850ef500..2aa7a672c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -38,6 +38,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonElement] public string FileName { get; set; } + [BsonIgnoreIfDefault] + [BsonElement] + public string FileHash { get; set; } + [BsonIgnoreIfDefault] [BsonElement] public string Slug { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index e203ac95d..07b7eb57e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -46,7 +46,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .Ascending(x => x.Tags) .Descending(x => x.LastModified)), new CreateIndexModel( - Index.Ascending(x => x.Slug)) + Index + .Ascending(x => x.AppId) + .Ascending(x => x.IsDeleted) + .Ascending(x => x.FileHash)), + new CreateIndexModel( + Index + .Ascending(x => x.AppId) + .Ascending(x => x.Slug)) }, ct); } @@ -102,19 +109,41 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task FindAssetAsync(string slug) + public async Task FindAssetBySlugAsync(Guid appId, string slug) { using (Profiler.TraceMethod()) { var assetEntity = - await Collection.Find(x => x.Slug == slug) + await Collection.Find(x => x.IndexedAppId == appId && x.Slug == slug) .FirstOrDefaultAsync(); + if (assetEntity?.IsDeleted == true) + { + return null; + } + return assetEntity; } } - public async Task FindAssetAsync(Guid id) + public async Task FindAssetByHashAsync(Guid appId, string hash) + { + using (Profiler.TraceMethod()) + { + var assetEntity = + await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash) + .FirstOrDefaultAsync(); + + if (assetEntity?.IsDeleted == true) + { + return null; + } + + return assetEntity; + } + } + + public async Task FindAssetAsync(Guid id, bool allowDeleted = false) { using (Profiler.TraceMethod()) { @@ -122,6 +151,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets await Collection.Find(x => x.Id == id) .FirstOrDefaultAsync(); + if (assetEntity?.IsDeleted == true && !allowDeleted) + { + return null; + } + return assetEntity; } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 569ad468e..a1176be44 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -14,6 +14,8 @@ using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; +#pragma warning disable IDE0060 // Remove unused parameter + namespace Squidex.Domain.Apps.Entities.Apps.State { [CollectionName("Apps")] diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index c1b217806..dd15cdd28 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -7,9 +7,11 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; @@ -20,21 +22,25 @@ namespace Squidex.Domain.Apps.Entities.Assets public sealed class AssetCommandMiddleware : GrainCommandMiddleware { private readonly IAssetStore assetStore; + private readonly AssetQueryService assetQueryService; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IEnumerable> tagGenerators; public AssetCommandMiddleware( IGrainFactory grainFactory, + AssetQueryService assetQueryService, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator, IEnumerable> tagGenerators) : base(grainFactory) { Guard.NotNull(assetStore, nameof(assetStore)); + Guard.NotNull(assetQueryService, nameof(assetQueryService)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(tagGenerators, nameof(tagGenerators)); this.assetStore = assetStore; + this.assetQueryService = assetQueryService; this.assetThumbnailGenerator = assetThumbnailGenerator; this.tagGenerators = tagGenerators; @@ -53,21 +59,45 @@ namespace Squidex.Domain.Apps.Entities.Assets createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead()); - foreach (var tagGenerator in tagGenerators) - { - tagGenerator.GenerateTags(createAsset, createAsset.Tags); - } + createAsset.FileHash = await UploadAsync(context, createAsset.File); - var originalTags = new HashSet(createAsset.Tags); - - await assetStore.UploadAsync(context.ContextId.ToString(), createAsset.File.OpenRead()); try { - var result = (AssetSavedResult)await ExecuteCommandAsync(createAsset); - - context.Complete(new AssetCreatedResult(createAsset.AssetId, originalTags, result.Version)); + var existing = await assetQueryService.FindAssetByHashAsync(createAsset.AppId.Id, createAsset.FileHash); + + AssetCreatedResult result; + + if (IsDuplicate(createAsset, existing)) + { + result = new AssetCreatedResult( + existing.Id, + existing.Tags, + existing.Version, + existing.FileVersion, + existing.FileHash, + true); + } + else + { + foreach (var tagGenerator in tagGenerators) + { + tagGenerator.GenerateTags(createAsset, createAsset.Tags); + } + + var commandResult = (AssetSavedResult)await ExecuteCommandAsync(createAsset); + + result = new AssetCreatedResult( + createAsset.AssetId, + createAsset.Tags, + commandResult.Version, + commandResult.FileVersion, + commandResult.FileHash, + false); + + await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null); + } - await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null); + context.Complete(result); } finally { @@ -81,10 +111,10 @@ namespace Squidex.Domain.Apps.Entities.Assets { updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead()); - await assetStore.UploadAsync(context.ContextId.ToString(), updateAsset.File.OpenRead()); + updateAsset.FileHash = await UploadAsync(context, updateAsset.File); try { - var result = await ExecuteCommandAsync(updateAsset) as AssetSavedResult; + var result = (AssetSavedResult)await ExecuteCommandAsync(updateAsset); context.Complete(result); @@ -103,5 +133,24 @@ namespace Squidex.Domain.Apps.Entities.Assets break; } } + + private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset) + { + return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize; + } + + private async Task UploadAsync(CommandContext context, AssetFile file) + { + string hash; + + using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256)) + { + await assetStore.UploadAsync(context.ContextId.ToString(), hashStream); + + hash = hashStream.GetHashStringAndReset(); + } + + return hash; + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs index 8abb01c95..de8da5f23 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs @@ -11,18 +11,25 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetCreatedResult : EntitySavedResult + public sealed class AssetCreatedResult : EntityCreatedResult { - public Guid Id { get; } - public HashSet Tags { get; } - public AssetCreatedResult(Guid id, HashSet tags, long version) - : base(version) - { - Id = id; + public long FileVersion { get; } + + public string FileHash { get; } + + public bool IsDuplicate { get; } + public AssetCreatedResult(Guid id, HashSet tags, long version, long fileVersion, string fileHash, bool isDuplicate) + : base(id, version) + { Tags = tags; + + FileVersion = fileVersion; + FileHash = fileHash; + + IsDuplicate = isDuplicate; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index ec7b92db2..b16b18ae4 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -47,11 +47,11 @@ namespace Squidex.Domain.Apps.Entities.Assets { GuardAsset.CanCreate(c); - c.Tags = await NormalizeTagsAsync(c.AppId.Id, c.Tags); + var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags); - Create(c); + Create(c, tagIds); - return new AssetSavedResult(Version, Snapshot.FileVersion); + return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash); }); case UpdateAsset updateRule: return UpdateAsync(updateRule, c => @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets Update(c); - return new AssetSavedResult(Version, Snapshot.FileVersion); + return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash); }); case DeleteAsset deleteAsset: return UpdateAsync(deleteAsset, async c => @@ -76,12 +76,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug); - if (c.Tags != null) - { - c.Tags = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); - } + var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags); - Annotate(c); + Annotate(c, tagIds); }); default: throw new NotSupportedException(); @@ -90,32 +87,37 @@ namespace Squidex.Domain.Apps.Entities.Assets private async Task> NormalizeTagsAsync(Guid appId, HashSet tags) { + if (tags == null) + { + return null; + } + var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags); return new HashSet(normalized.Values); } - public void Create(CreateAsset command) + public void Create(CreateAsset command, HashSet tagIds) { var @event = SimpleMapper.Map(command, new AssetCreated { + IsImage = command.ImageInfo != null, FileName = command.File.FileName, FileSize = command.File.FileSize, FileVersion = 0, MimeType = command.File.MimeType, PixelWidth = command.ImageInfo?.PixelWidth, PixelHeight = command.ImageInfo?.PixelHeight, - IsImage = command.ImageInfo != null, Slug = command.File.FileName.ToAssetSlug() }); + @event.Tags = tagIds; + RaiseEvent(@event); } public void Update(UpdateAsset command) { - VerifyNotDeleted(); - var @event = SimpleMapper.Map(command, new AssetUpdated { FileVersion = Snapshot.FileVersion + 1, @@ -129,14 +131,18 @@ namespace Squidex.Domain.Apps.Entities.Assets RaiseEvent(@event); } - public void Delete(DeleteAsset command) + public void Annotate(AnnotateAsset command, HashSet tagIds) { - RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); + var @event = SimpleMapper.Map(command, new AssetAnnotated()); + + @event.Tags = tagIds; + + RaiseEvent(@event); } - public void Annotate(AnnotateAsset command) + public void Delete(DeleteAsset command) { - RaiseEvent(SimpleMapper.Map(command, new AssetAnnotated())); + RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); } private void RaiseEvent(AppEvent @event) diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index 0bd7a6975..5965d1030 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.Queries.OData; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetQueryService : IAssetQueryService + public class AssetQueryService : IAssetQueryService { private readonly ITagService tagService; private readonly IAssetRepository assetRepository; @@ -38,21 +38,38 @@ namespace Squidex.Domain.Apps.Entities.Assets this.tagService = tagService; } - public async Task FindAssetAsync(QueryContext context, Guid id) + public virtual Task FindAssetAsync(QueryContext context, Guid id) { Guard.NotNull(context, nameof(context)); + return FindAssetAsync(context.App.Id, id); + } + + public virtual async Task FindAssetAsync(Guid appId, Guid id) + { var asset = await assetRepository.FindAssetAsync(id); if (asset != null) { - await DenormalizeTagsAsync(context.App.Id, Enumerable.Repeat(asset, 1)); + await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1)); + } + + return asset; + } + + public virtual async Task FindAssetByHashAsync(Guid appId, string hash) + { + var asset = await assetRepository.FindAssetByHashAsync(appId, hash); + + if (asset != null) + { + await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1)); } return asset; } - public async Task> QueryAsync(QueryContext context, Q query) + public virtual async Task> QueryAsync(QueryContext context, Q query) { Guard.NotNull(context, nameof(context)); Guard.NotNull(query, nameof(query)); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs index a07331e8f..a43e109cc 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs @@ -13,10 +13,13 @@ namespace Squidex.Domain.Apps.Entities.Assets { public long FileVersion { get; } - public AssetSavedResult(long version, long fileVersion) + public string FileHash { get; } + + public AssetSavedResult(long version, long fileVersion, string fileHash) : base(version) { FileVersion = fileVersion; + FileHash = fileHash; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 829cf0ce5..9c49e67bd 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -22,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public HashSet Tags { get; set; } + public string FileHash { get; set; } + public CreateAsset() { AssetId = Guid.NewGuid(); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs index 1bb193419..1c998ac7a 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs @@ -14,5 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public AssetFile File { get; set; } public ImageInfo ImageInfo { get; set; } + + public string FileHash { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index 939fc65ab..81481f99b 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -19,9 +19,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories Task> QueryAsync(Guid appId, HashSet ids); - Task FindAssetAsync(string slug); + Task FindAssetAsync(Guid id, bool allowDeleted = false); - Task FindAssetAsync(Guid id); + Task FindAssetBySlugAsync(Guid appId, string slug); + + Task FindAssetByHashAsync(Guid appId, string hash); Task RemoveAsync(Guid appId); } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs index 2ea47f7ab..142b5075f 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs @@ -16,6 +16,8 @@ using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; +#pragma warning disable IDE0060 // Remove unused parameter + namespace Squidex.Domain.Apps.Entities.Assets.State { public class AssetState : DomainObjectState, IAssetEntity @@ -26,6 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State [DataMember] public string FileName { get; set; } + [DataMember] + public string FileHash { get; set; } + [DataMember] public string MimeType { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index ca610a74c..1f2169431 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -15,6 +15,8 @@ using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; +#pragma warning disable IDE0060 // Remove unused parameter + namespace Squidex.Domain.Apps.Entities.Contents.State { public class ContentState : DomainObjectState, IContentEntity diff --git a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs index 48e354328..c6e1bd49d 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/State/RuleState.cs @@ -15,6 +15,8 @@ using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; +#pragma warning disable IDE0060 // Remove unused parameter + namespace Squidex.Domain.Apps.Entities.Rules.State { [CollectionName("Rules")] diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 922da11a3..9bde80fd1 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -16,6 +16,8 @@ using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; +#pragma warning disable IDE0060 // Remove unused parameter + namespace Squidex.Domain.Apps.Entities.Schemas.State { [CollectionName("Schemas")] diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs index 568c324e0..5200031cc 100644 --- a/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs @@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Events.Assets { public string FileName { get; set; } + public string FileHash { get; set; } + public string MimeType { get; set; } public string Slug { get; set; } diff --git a/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs b/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs index aca1cb89b..b26c49397 100644 --- a/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs +++ b/src/Squidex.Domain.Apps.Events/Assets/AssetUpdated.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Events.Assets { public string MimeType { get; set; } + public string FileHash { get; set; } + public long FileSize { get; set; } public long FileVersion { get; set; } diff --git a/src/Squidex.Infrastructure/Assets/HasherStream.cs b/src/Squidex.Infrastructure/Assets/HasherStream.cs new file mode 100644 index 000000000..ea11e8682 --- /dev/null +++ b/src/Squidex.Infrastructure/Assets/HasherStream.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class HasherStream : Stream + { + private readonly Stream inner; + private readonly IncrementalHash hasher; + + public override bool CanRead + { + get { return inner.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return inner.Length; } + } + + public override long Position + { + get { return inner.Position; } + set { throw new NotSupportedException(); } + } + + public HasherStream(Stream inner, HashAlgorithmName hashAlgorithmName) + { + Guard.NotNull(inner, nameof(inner)); + + this.inner = inner; + + hasher = IncrementalHash.CreateHash(hashAlgorithmName); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var read = inner.Read(buffer, offset, count); + + if (read > 0) + { + hasher.AppendData(buffer, offset, read); + } + + return read; + } + + public byte[] GetHashAndReset() + { + return hasher.GetHashAndReset(); + } + + public string GetHashStringAndReset() + { + return Convert.ToBase64String(GetHashAndReset()); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs index c5c2cbce3..10fa7fe84 100644 --- a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Assets { - public sealed class MemoryAssetStore : IAssetStore + public class MemoryAssetStore : IAssetStore { private readonly ConcurrentDictionary streams = new ConcurrentDictionary(); private readonly AsyncLock readerLock = new AsyncLock(); @@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Assets return null; } - public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) + public virtual async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) { Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); @@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Assets } } - public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) + public virtual async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) { Guard.NotNullOrEmpty(fileName, nameof(fileName)); @@ -62,7 +62,7 @@ namespace Squidex.Infrastructure.Assets } } - public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) + public virtual async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) { Guard.NotNullOrEmpty(fileName, nameof(fileName)); @@ -99,7 +99,7 @@ namespace Squidex.Infrastructure.Assets } } - public Task DeleteAsync(string fileName) + public virtual Task DeleteAsync(string fileName) { Guard.NotNullOrEmpty(fileName, nameof(fileName)); diff --git a/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs b/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs index 324c8c6a4..2ab583d59 100644 --- a/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs +++ b/src/Squidex.Infrastructure/Commands/EntityCreatedResult{T}.cs @@ -7,7 +7,7 @@ namespace Squidex.Infrastructure.Commands { - public sealed class EntityCreatedResult : EntitySavedResult + public class EntityCreatedResult : EntitySavedResult { public T IdOrValue { get; } diff --git a/src/Squidex.Infrastructure/RandomHash.cs b/src/Squidex.Infrastructure/RandomHash.cs index 9b395e23a..e64115342 100644 --- a/src/Squidex.Infrastructure/RandomHash.cs +++ b/src/Squidex.Infrastructure/RandomHash.cs @@ -19,11 +19,15 @@ namespace Squidex.Infrastructure } public static string Sha256Base64(this string value) + { + return Sha256Base64(Encoding.UTF8.GetBytes(value)); + } + + public static string Sha256Base64(this byte[] bytes) { using (var sha = SHA256.Create()) { - var bytesValue = Encoding.UTF8.GetBytes(value); - var bytesHash = sha.ComputeHash(bytesValue); + var bytesHash = sha.ComputeHash(bytes); var result = Convert.ToBase64String(bytesHash); diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index c9ff22e9b..056250ff0 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -62,7 +62,38 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("assets/{id}/{*more}")] [ProducesResponseType(typeof(FileResult), 200)] [ApiCosts(0.5)] - public async Task GetAssetContent(string id, string more, + public async Task GetAssetContent(Guid id, string more, + [FromQuery] long version = EtagVersion.Any, + [FromQuery] int? width = null, + [FromQuery] int? height = null, + [FromQuery] int? quality = null, + [FromQuery] string mode = null) + { + var entity = await assetRepository.FindAssetAsync(id); + + return DeliverAsset(entity, version, width, height, quality, mode); + } + + /// + /// Get the asset content. + /// + /// The name of the app. + /// The id or slug of the asset. + /// Optional suffix that can be used to seo-optimize the link to the image Has not effect. + /// The optional version of the asset. + /// The target width of the asset, if it is an image. + /// The target height of the asset, if it is an image. + /// Optional image quality, it is is an jpeg image. + /// The resize mode when the width and height is defined. + /// + /// 200 => Asset found and content or (resized) image returned. + /// 404 => Asset or app not found. + /// + [HttpGet] + [Route("assets/{app}/{idOrSlug}/{*more}")] + [ProducesResponseType(typeof(FileResult), 200)] + [ApiCosts(0.5)] + public async Task GetAssetContent(string app, string idOrSlug, string more, [FromQuery] long version = EtagVersion.Any, [FromQuery] int? width = null, [FromQuery] int? height = null, @@ -71,15 +102,20 @@ namespace Squidex.Areas.Api.Controllers.Assets { IAssetEntity entity; - if (Guid.TryParse(id, out var guid)) + if (Guid.TryParse(idOrSlug, out var guid)) { entity = await assetRepository.FindAssetAsync(guid); } else { - entity = await assetRepository.FindAssetAsync(id); + entity = await assetRepository.FindAssetByHashAsync(App.Id, idOrSlug); } + return DeliverAsset(entity, version, width, height, quality, mode); + } + + private IActionResult DeliverAsset(IAssetEntity entity, long version, int? width, int? height, int? quality, string mode) + { if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0) { return NotFound(); diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs index 0b1807312..da76057c0 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetCreatedDto.cs @@ -76,6 +76,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// public int? PixelHeight { get; set; } + /// + /// Indicates if the asset has been already uploaded. + /// + public bool IsDuplicate { get; set; } + /// /// The version of the asset. /// @@ -83,22 +88,21 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models public static AssetCreatedDto FromCommand(CreateAsset command, AssetCreatedResult result) { - var response = new AssetCreatedDto + return new AssetCreatedDto { - Id = command.AssetId, + Id = result.IdOrValue, FileName = command.File.FileName, FileSize = command.File.FileSize, FileType = command.File.FileName.FileType(), - FileVersion = result.Version, + FileVersion = result.FileVersion, MimeType = command.File.MimeType, IsImage = command.ImageInfo != null, + IsDuplicate = result.IsDuplicate, PixelWidth = command.ImageInfo?.PixelWidth, PixelHeight = command.ImageInfo?.PixelHeight, Tags = result.Tags, Version = result.Version }; - - return response; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index e13b76993..1053302be 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -29,6 +29,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Required] public string FileName { get; set; } + /// + /// The file hash. + /// + [Required] + public string FileHash { get; set; } + /// /// The slug. /// diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs index cb6fcb801..e65faf494 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs @@ -19,6 +19,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [Required] public string MimeType { get; set; } + /// + /// The file hash. + /// + [Required] + public string FileHash { get; set; } + /// /// The size of the file in bytes. /// diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 79983c5dc..a6db529aa 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -153,6 +153,6 @@ - $(NoWarn);CS1591;1591;1573;1572;NU1605 + $(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060 \ No newline at end of file diff --git a/src/Squidex/app/features/administration/declarations.ts b/src/Squidex/app/features/administration/declarations.ts index 09279fe2c..2a3b2198d 100644 --- a/src/Squidex/app/features/administration/declarations.ts +++ b/src/Squidex/app/features/administration/declarations.ts @@ -19,4 +19,5 @@ export * from './services/event-consumers.service'; export * from './services/users.service'; export * from './state/event-consumers.state'; +export * from './state/users.forms'; export * from './state/users.state'; \ No newline at end of file diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index a8d339148..30b65c600 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -21,7 +21,7 @@ import { EventConsumersState } from './../../state/event-consumers.state'; }) export class EventConsumersPageComponent extends ResourceOwner implements OnInit { public eventConsumerErrorDialog = new DialogModel(); - public eventConsumerError = ''; + public eventConsumerError?: string; constructor( public readonly eventConsumersState: EventConsumersState diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.ts b/src/Squidex/app/features/administration/services/event-consumers.service.ts index 74de8309a..169850008 100644 --- a/src/Squidex/app/features/administration/services/event-consumers.service.ts +++ b/src/Squidex/app/features/administration/services/event-consumers.service.ts @@ -16,20 +16,16 @@ import { pretifyError } from '@app/shared'; -export class EventConsumerDto extends Model { +export class EventConsumerDto extends Model { constructor( public readonly name: string, - public readonly isStopped: boolean, - public readonly isResetting: boolean, - public readonly error: string, - public readonly position: string + public readonly isStopped?: boolean, + public readonly isResetting?: boolean, + public readonly error?: string, + public readonly position?: string ) { super(); } - - public with(value: Partial): EventConsumerDto { - return this.clone(value); - } } @Injectable() diff --git a/src/Squidex/app/features/administration/services/users.service.ts b/src/Squidex/app/features/administration/services/users.service.ts index 32152110b..524f4b5d4 100644 --- a/src/Squidex/app/features/administration/services/users.service.ts +++ b/src/Squidex/app/features/administration/services/users.service.ts @@ -13,25 +13,19 @@ import { map } from 'rxjs/operators'; import { ApiUrlConfig, Model, - pretifyError + pretifyError, + ResultSet } from '@app/shared'; -export class UsersDto extends Model { - constructor( - public readonly total: number, - public readonly items: UserDto[] - ) { - super(); - } -} +export class UsersDto extends ResultSet {} -export class UserDto extends Model { +export class UserDto extends Model { constructor( public readonly id: string, public readonly email: string, public readonly displayName: string, - public readonly permissions: string[], - public readonly isLocked: boolean + public readonly permissions: string[] = [], + public readonly isLocked?: boolean ) { super(); } @@ -41,24 +35,18 @@ export class UserDto extends Model { } } -export class CreateUserDto { - constructor( - public readonly email: string, - public readonly displayName: string, - public readonly permissions: string[], - public readonly password: string - ) { - } +export interface CreateUserDto { + readonly email: string; + readonly displayName: string; + readonly permissions: string[]; + readonly password: string; } -export class UpdateUserDto { - constructor( - public readonly email: string, - public readonly displayName: string, - public readonly permissions: string[], - public readonly password?: string - ) { - } +export interface UpdateUserDto { + readonly email: string; + readonly displayName: string; + readonly permissions: string[]; + readonly password?: string; } @Injectable() diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts index 980ba1e92..c288609e7 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts +++ b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts @@ -16,8 +16,8 @@ import { EventConsumersState } from './event-consumers.state'; describe('EventConsumersState', () => { const oldConsumers = [ - new EventConsumerDto('name1', false, false, 'error', '1'), - new EventConsumerDto('name2', true, true, 'error', '2') + new EventConsumerDto('name1', false), + new EventConsumerDto('name2', true) ]; let dialogs: IMock; diff --git a/src/Squidex/app/features/administration/state/users.forms.ts b/src/Squidex/app/features/administration/state/users.forms.ts new file mode 100644 index 000000000..e998d449f --- /dev/null +++ b/src/Squidex/app/features/administration/state/users.forms.ts @@ -0,0 +1,60 @@ +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +import { Form, ValidatorsEx } from '@app/shared'; + +import { UserDto } from './../services/users.service'; + +export class UserForm extends Form { + constructor( + formBuilder: FormBuilder + ) { + super(formBuilder.group({ + email: ['', + [ + Validators.email, + Validators.required, + Validators.maxLength(100) + ] + ], + displayName: ['', + [ + Validators.required, + Validators.maxLength(100) + ] + ], + password: ['', + [ + Validators.nullValidator + ] + ], + passwordConfirm: ['', + [ + ValidatorsEx.match('password', 'Passwords must be the same.') + ] + ], + permissions: [''] + })); + } + + public load(user?: UserDto) { + if (user) { + this.form.controls['password'].setValidators(null); + + super.load({ ...user, permissions: user.permissions.join('\n') }); + } else { + this.form.controls['password'].setValidators(Validators.required); + + super.load(undefined); + } + } + + public submit() { + const result = super.submit(); + + if (result) { + result['permissions'] = result['permissions'].split('\n').filter((x: any) => !!x); + } + + return result; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/src/Squidex/app/features/administration/state/users.state.spec.ts index 31adfec4d..53bd4f3e3 100644 --- a/src/Squidex/app/features/administration/state/users.state.spec.ts +++ b/src/Squidex/app/features/administration/state/users.state.spec.ts @@ -13,8 +13,6 @@ import { AuthService, DialogService } from '@app/shared'; import { UsersState } from './users.state'; import { - CreateUserDto, - UpdateUserDto, UserDto, UsersDto, UsersService @@ -168,7 +166,7 @@ describe('UsersState', () => { }); it('should update user properties when updated', () => { - const request = new UpdateUserDto('new@mail.com', 'New', ['Permission1']); + const request = { email: 'new@mail.com', displayName: 'New', permissions: ['Permission1'] }; usersService.setup(x => x.putUser('id1', request)) .returns(() => of({})); @@ -178,13 +176,14 @@ describe('UsersState', () => { const user_1 = usersState.snapshot.users.at(0); - expect(user_1.user.email).toEqual('new@mail.com'); - expect(user_1.user.displayName).toEqual('New'); + expect(user_1.user.email).toEqual(request.email); + expect(user_1.user.displayName).toEqual(request.permissions); + expect(user_1.user.permissions).toEqual(request.permissions); expect(user_1).toBe(usersState.snapshot.selectedUser!); }); it('should add user to snapshot when created', () => { - const request = new CreateUserDto(newUser.email, newUser.displayName, newUser.permissions, 'password'); + const request = { ...newUser, password: 'password' }; usersService.setup(x => x.postUser(request)) .returns(() => of(newUser)); diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index bb34bc5f8..c5d82e70e 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/src/Squidex/app/features/administration/state/users.state.ts @@ -6,7 +6,6 @@ */ import { Injectable } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; @@ -15,12 +14,10 @@ import '@app/framework/utils/rxjs-extensions'; import { AuthService, DialogService, - Form, ImmutableArray, notify, Pager, - State, - ValidatorsEx + State } from '@app/shared'; import { @@ -30,61 +27,6 @@ import { UsersService } from './../services/users.service'; -export class UserForm extends Form { - constructor( - formBuilder: FormBuilder - ) { - super(formBuilder.group({ - email: ['', - [ - Validators.email, - Validators.required, - Validators.maxLength(100) - ] - ], - displayName: ['', - [ - Validators.required, - Validators.maxLength(100) - ] - ], - password: ['', - [ - Validators.nullValidator - ] - ], - passwordConfirm: ['', - [ - ValidatorsEx.match('password', 'Passwords must be the same.') - ] - ], - permissions: [''] - })); - } - - public load(user?: UserDto) { - if (user) { - this.form.controls['password'].setValidators(null); - - super.load({ ...user, permissions: user.permissions.join('\n') }); - } else { - this.form.controls['password'].setValidators(Validators.required); - - super.load(undefined); - } - } - - public submit() { - const result = super.submit(); - - if (result) { - result['permissions'] = result['permissions'].split('\n').filter((x: any) => !!x); - } - - return result; - } -} - interface SnapshotUser { // The user. user: UserDto; diff --git a/src/Squidex/app/framework/state.ts b/src/Squidex/app/framework/state.ts index f93c0b7c3..ae0ddfcb1 100644 --- a/src/Squidex/app/framework/state.ts +++ b/src/Squidex/app/framework/state.ts @@ -97,9 +97,17 @@ export class Form { } } -export class Model { - protected clone(update: ((v: any) => object) | object, validOnly = false): any { - let values: object; +export function createModel(c: { new(): T; }, values: Partial): T { + return Object.assign(new c(), values); +} + +export class Model { + public with(value: Partial, validOnly = false): T { + return this.clone(value, validOnly); + } + + protected clone(update: ((v: any) => T) | Partial, validOnly = false): T { + let values: Partial; if (Types.isFunction(update)) { values = update(this); } else { @@ -126,6 +134,15 @@ export class Model { } } +export class ResultSet extends Model> { + constructor( + public readonly total: number, + public readonly items: T[] + ) { + super(); + } +} + export class State { private readonly state: BehaviorSubject>; private readonly initialState: Readonly; diff --git a/src/Squidex/app/shared/components/assets-list.component.ts b/src/Squidex/app/shared/components/assets-list.component.ts index dd2339e48..674656236 100644 --- a/src/Squidex/app/shared/components/assets-list.component.ts +++ b/src/Squidex/app/shared/components/assets-list.component.ts @@ -11,6 +11,7 @@ import { onErrorResumeNext } from 'rxjs/operators'; import { AssetDto, AssetsState, + DialogService, ImmutableArray } from '@app/shared/internal'; @@ -38,10 +39,19 @@ export class AssetsListComponent { @Output() public select = new EventEmitter(); + constructor( + private readonly dialogs: DialogService + ) { + } + public add(file: File, asset: AssetDto) { this.newFiles = this.newFiles.remove(file); - this.state.add(asset); + if (asset.isDuplicate) { + this.dialogs.notifyError('The same asset has already been uploaded.'); + } else { + this.state.add(asset); + } } public search() { diff --git a/src/Squidex/app/shared/services/app-clients.service.spec.ts b/src/Squidex/app/shared/services/app-clients.service.spec.ts index 1137473a9..c1179f696 100644 --- a/src/Squidex/app/shared/services/app-clients.service.spec.ts +++ b/src/Squidex/app/shared/services/app-clients.service.spec.ts @@ -15,8 +15,6 @@ import { AppClientDto, AppClientsDto, AppClientsService, - CreateAppClientDto, - UpdateAppClientDto, Version } from './../'; @@ -83,7 +81,7 @@ describe('AppClientsService', () => { it('should make post request to create client', inject([AppClientsService, HttpTestingController], (appClientsService: AppClientsService, httpMock: HttpTestingController) => { - const dto = new CreateAppClientDto('client1'); + const dto = { id: 'client1' }; let client: AppClientDto; @@ -104,7 +102,7 @@ describe('AppClientsService', () => { it('should make put request to rename client', inject([AppClientsService, HttpTestingController], (appClientsService: AppClientsService, httpMock: HttpTestingController) => { - const dto = new UpdateAppClientDto('Client 1 New'); + const dto = { name: 'New Name' }; appClientsService.putClient('my-app', 'client1', dto, version).subscribe(); diff --git a/src/Squidex/app/shared/services/app-clients.service.ts b/src/Squidex/app/shared/services/app-clients.service.ts index f7dfd3590..36507531d 100644 --- a/src/Squidex/app/shared/services/app-clients.service.ts +++ b/src/Squidex/app/shared/services/app-clients.service.ts @@ -20,7 +20,7 @@ import { Versioned } from '@app/framework'; -export class AppClientsDto extends Model { +export class AppClientsDto extends Model { constructor( public readonly clients: AppClientDto[], public readonly version: Version @@ -29,42 +29,32 @@ export class AppClientsDto extends Model { } } -export class AppClientDto extends Model { +export class AppClientDto extends Model { constructor( public readonly id: string, public readonly name: string, public readonly secret: string, - public readonly role: string + public readonly role = 'Developer' ) { super(); } - - public with(value: Partial): AppClientDto { - return this.clone(value); - } } -export class CreateAppClientDto { +export class AccessTokenDto { constructor( - public readonly id: string + public readonly accessToken: string, + public readonly tokenType: string ) { } } -export class UpdateAppClientDto { - constructor( - public readonly name?: string, - public readonly role?: string - ) { - } +export interface CreateAppClientDto { + readonly id: string; } -export class AccessTokenDto { - constructor( - public readonly accessToken: string, - public readonly tokenType: string - ) { - } +export interface UpdateAppClientDto { + readonly name?: string; + readonly role?: string; } @Injectable() diff --git a/src/Squidex/app/shared/services/app-contributors.service.spec.ts b/src/Squidex/app/shared/services/app-contributors.service.spec.ts index fda899774..084240dfe 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.spec.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.spec.ts @@ -14,7 +14,6 @@ import { AppContributorDto, AppContributorsDto, AppContributorsService, - AssignContributorDto, ContributorAssignedDto, Version } from './../'; @@ -81,7 +80,7 @@ describe('AppContributorsService', () => { it('should make post request to assign contributor', inject([AppContributorsService, HttpTestingController], (appContributorsService: AppContributorsService, httpMock: HttpTestingController) => { - const dto = new AssignContributorDto('123', 'Owner'); + const dto = { contributorId: '123', role: 'Owner' }; let contributorAssignedDto: ContributorAssignedDto; diff --git a/src/Squidex/app/shared/services/app-contributors.service.ts b/src/Squidex/app/shared/services/app-contributors.service.ts index c67966b79..ddf3edd6c 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.ts @@ -20,7 +20,7 @@ import { Versioned } from '@app/framework'; -export class AppContributorsDto extends Model { +export class AppContributorsDto extends Model { constructor( public readonly contributors: AppContributorDto[], public readonly maxContributors: number, @@ -30,31 +30,24 @@ export class AppContributorsDto extends Model { } } -export class AssignContributorDto extends Model { +export class AppContributorDto extends Model { constructor( public readonly contributorId: string, - public readonly role: string, - public readonly invite = false + public readonly role: string ) { super(); } } -export class AppContributorDto extends Model { - constructor( - public readonly contributorId: string, - public readonly role: string - ) { - super(); - } +export interface ContributorAssignedDto { + readonly contributorId: string; + readonly isCreated?: boolean; } -export class ContributorAssignedDto { - constructor( - public readonly contributorId: string, - public readonly isCreated: boolean - ) { - } +export interface AssignContributorDto { + readonly contributorId: string; + readonly role: string; + readonly invite?: boolean; } @Injectable() @@ -93,9 +86,7 @@ export class AppContributorsService { map(response => { const body: any = response.payload.body; - const result = new ContributorAssignedDto(body.contributorId, body.isCreated); - - return new Versioned(response.version, result); + return new Versioned(response.version, body); }), tap(() => { this.analytics.trackEvent('Contributor', 'Configured', appName); diff --git a/src/Squidex/app/shared/services/app-languages.service.spec.ts b/src/Squidex/app/shared/services/app-languages.service.spec.ts index b17284dc7..5fd7d0cab 100644 --- a/src/Squidex/app/shared/services/app-languages.service.spec.ts +++ b/src/Squidex/app/shared/services/app-languages.service.spec.ts @@ -9,13 +9,11 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { inject, TestBed } from '@angular/core/testing'; import { - AddAppLanguageDto, AnalyticsService, ApiUrlConfig, AppLanguageDto, AppLanguagesDto, AppLanguagesService, - UpdateAppLanguageDto, Version } from './../'; @@ -83,7 +81,7 @@ describe('AppLanguagesService', () => { it('should make post request to add language', inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { - const dto = new AddAppLanguageDto('de'); + const dto = { language: 'de' }; let language: AppLanguageDto; @@ -104,7 +102,7 @@ describe('AppLanguagesService', () => { it('should make put request to make master language', inject([AppLanguagesService, HttpTestingController], (appLanguagesService: AppLanguagesService, httpMock: HttpTestingController) => { - const dto = new UpdateAppLanguageDto(true, true, []); + const dto = { isMaster: true }; appLanguagesService.putLanguage('my-app', 'de', dto, version).subscribe(); diff --git a/src/Squidex/app/shared/services/app-languages.service.ts b/src/Squidex/app/shared/services/app-languages.service.ts index 7f68eaa6a..8cd281d7c 100644 --- a/src/Squidex/app/shared/services/app-languages.service.ts +++ b/src/Squidex/app/shared/services/app-languages.service.ts @@ -20,7 +20,7 @@ import { Versioned } from '@app/framework'; -export class AppLanguagesDto extends Model { +export class AppLanguagesDto extends Model { constructor( public readonly languages: AppLanguageDto[], public readonly version: Version @@ -29,7 +29,7 @@ export class AppLanguagesDto extends Model { } } -export class AppLanguageDto extends Model { +export class AppLanguageDto extends Model { constructor( public readonly iso2Code: string, public readonly englishName: string, @@ -41,20 +41,14 @@ export class AppLanguageDto extends Model { } } -export class AddAppLanguageDto { - constructor( - public readonly language: string - ) { - } +export interface AddAppLanguageDto { + readonly language: string; } -export class UpdateAppLanguageDto { - constructor( - public readonly isMaster: boolean, - public readonly isOptional: boolean, - public readonly fallback: string[] - ) { - } +export interface UpdateAppLanguageDto { + readonly isMaster?: boolean; + readonly isOptional?: boolean; + readonly fallback?: string[]; } @Injectable() diff --git a/src/Squidex/app/shared/services/app-patterns.service.spec.ts b/src/Squidex/app/shared/services/app-patterns.service.spec.ts index b5a31ab66..43b1b8e95 100644 --- a/src/Squidex/app/shared/services/app-patterns.service.spec.ts +++ b/src/Squidex/app/shared/services/app-patterns.service.spec.ts @@ -14,7 +14,6 @@ import { AppPatternDto, AppPatternsDto, AppPatternsService, - EditAppPatternDto, Version } from './../'; @@ -80,7 +79,7 @@ describe('AppPatternsService', () => { it('should make post request to add pattern', inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => { - const dto = new EditAppPatternDto('Number', '[0-9]', 'Message1'); + const dto = { name: 'Number', pattern: '[0-9]' }; let pattern: AppPatternDto; @@ -106,7 +105,7 @@ describe('AppPatternsService', () => { it('should make put request to update pattern', inject([AppPatternsService, HttpTestingController], (patternService: AppPatternsService, httpMock: HttpTestingController) => { - const dto = new EditAppPatternDto('Number', '[0-9]', 'Message1'); + const dto = { name: 'Number', pattern: '[0-9]' }; patternService.putPattern('my-app', '1', dto, version).subscribe(); diff --git a/src/Squidex/app/shared/services/app-patterns.service.ts b/src/Squidex/app/shared/services/app-patterns.service.ts index 71cbe6fb6..d2ce8969c 100644 --- a/src/Squidex/app/shared/services/app-patterns.service.ts +++ b/src/Squidex/app/shared/services/app-patterns.service.ts @@ -20,7 +20,7 @@ import { Versioned } from '@app/framework'; -export class AppPatternsDto extends Model { +export class AppPatternsDto extends Model { constructor( public readonly patterns: AppPatternDto[], public readonly version: Version @@ -29,27 +29,23 @@ export class AppPatternsDto extends Model { } } -export class AppPatternDto extends Model { +export class AppPatternDto extends Model { constructor( public readonly id: string, public readonly name: string, public readonly pattern: string, - public readonly message: string + public readonly message?: string ) { super(); } } -export class EditAppPatternDto { - constructor( - public readonly name: string, - public readonly pattern: string, - public readonly message: string - ) { - } +export interface EditAppPatternDto { + readonly name: string; + readonly pattern: string; + readonly message?: string; } - @Injectable() export class AppPatternsService { constructor( diff --git a/src/Squidex/app/shared/services/app-roles.service.spec.ts b/src/Squidex/app/shared/services/app-roles.service.spec.ts index 1138a3b43..35979140b 100644 --- a/src/Squidex/app/shared/services/app-roles.service.spec.ts +++ b/src/Squidex/app/shared/services/app-roles.service.spec.ts @@ -14,8 +14,6 @@ import { AppRoleDto, AppRolesDto, AppRolesService, - CreateAppRoleDto, - UpdateAppRoleDto, Version } from './../'; @@ -101,7 +99,7 @@ describe('AppRolesService', () => { it('should make post request to add role', inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => { - const dto = new CreateAppRoleDto('Role3'); + const dto = { name: 'Role3' }; let role: AppRoleDto; @@ -122,7 +120,7 @@ describe('AppRolesService', () => { it('should make put request to update role', inject([AppRolesService, HttpTestingController], (roleService: AppRolesService, httpMock: HttpTestingController) => { - const dto = new UpdateAppRoleDto(['P4', 'P5']); + const dto = { permissions: ['P4', 'P5'] }; roleService.putRole('my-app', 'role1', dto, version).subscribe(); diff --git a/src/Squidex/app/shared/services/app-roles.service.ts b/src/Squidex/app/shared/services/app-roles.service.ts index bf1878572..008fd47fd 100644 --- a/src/Squidex/app/shared/services/app-roles.service.ts +++ b/src/Squidex/app/shared/services/app-roles.service.ts @@ -20,7 +20,7 @@ import { Versioned } from '@app/framework'; -export class AppRolesDto extends Model { +export class AppRolesDto extends Model { constructor( public readonly roles: AppRoleDto[], public readonly version: Version @@ -29,7 +29,7 @@ export class AppRolesDto extends Model { } } -export class AppRoleDto extends Model { +export class AppRoleDto extends Model { constructor( public readonly name: string, public readonly numClients: number, @@ -38,24 +38,14 @@ export class AppRoleDto extends Model { ) { super(); } - - public with(value: Partial): AppRoleDto { - return this.clone(value); - } } -export class CreateAppRoleDto { - constructor( - public readonly name: string - ) { - } +export interface CreateAppRoleDto { + readonly name: string; } -export class UpdateAppRoleDto { - constructor( - public readonly permissions: string[] - ) { - } +export interface UpdateAppRoleDto { + readonly permissions: string[]; } @Injectable() diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/src/Squidex/app/shared/services/apps.service.spec.ts index 7fc4a43c1..361d80a2d 100644 --- a/src/Squidex/app/shared/services/apps.service.spec.ts +++ b/src/Squidex/app/shared/services/apps.service.spec.ts @@ -13,7 +13,6 @@ import { ApiUrlConfig, AppDto, AppsService, - CreateAppDto, DateTime, Permission } from './../'; @@ -83,7 +82,7 @@ describe('AppsService', () => { it('should make post request to create app', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { - const dto = new CreateAppDto('new-app'); + const dto = { name: 'new-app' }; let app: AppDto; diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index 8f7644298..5771c9775 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -20,26 +20,23 @@ import { pretifyError } from '@app/framework'; -export class AppDto extends Model { +export class AppDto extends Model { constructor( public readonly id: string, public readonly name: string, public readonly permissions: Permission[], public readonly created: DateTime, public readonly lastModified: DateTime, - public readonly planName: string, - public readonly planUpgrade: string + public readonly planName?: string, + public readonly planUpgrade?: string ) { super(); } } -export class CreateAppDto { - constructor( - public readonly name: string, - public readonly template?: string - ) { - } +export interface CreateAppDto { + readonly name: string; + readonly template?: string; } @Injectable() diff --git a/src/Squidex/app/shared/services/assets.service.spec.ts b/src/Squidex/app/shared/services/assets.service.spec.ts index fb0c87344..16709cc4f 100644 --- a/src/Squidex/app/shared/services/assets.service.spec.ts +++ b/src/Squidex/app/shared/services/assets.service.spec.ts @@ -10,7 +10,6 @@ import { inject, TestBed } from '@angular/core/testing'; import { AnalyticsService, - AnnotateAssetDto, ApiUrlConfig, AssetDto, AssetReplacedDto, @@ -31,9 +30,9 @@ describe('AssetDto', () => { const newVersion = new Version('2'); it('should update tag property and user info when annnoting', () => { - const update = new AnnotateAssetDto('NewName.png', null, null); + const update = { fileName: 'New-Name.png' }; - const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'name.png', [], 'url', version); + const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'Hash', 'png', 1, 1, 'image/png', false, false, 1, 1, 'name.png', [], 'url', version); const asset_2 = asset_1.annnotate(update, modifier, newVersion, modified); expect(asset_2.fileName).toEqual('NewName.png'); @@ -45,11 +44,12 @@ describe('AssetDto', () => { }); it('should update file properties when uploading', () => { - const update = new AssetReplacedDto(2, 2, 'image/jpeg', true, 2, 2); + const update = new AssetReplacedDto('Hash New', 2, 2, 'image/jpeg', true, 2, 2); - const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'png', 1, 1, 'image/png', false, 1, 1, 'name.png', [], 'url', version); + const asset_1 = new AssetDto('1', creator, creator, creation, creation, 'Name.png', 'Hash', 'png', 1, 1, 'image/png', false, false, 1, 1, 'name.png', [], 'url', version); const asset_2 = asset_1.update(update, modifier, newVersion, modified); + expect(asset_2.fileHash).toEqual('Hash New'); expect(asset_2.fileSize).toEqual(2); expect(asset_2.fileVersion).toEqual(2); expect(asset_2.mimeType).toEqual('image/jpeg'); @@ -133,6 +133,7 @@ describe('AssetsService', () => { lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', fileName: 'My Asset1.png', + fileHash: 'My Hash1', fileType: 'png', fileSize: 1024, fileVersion: 2000, @@ -151,6 +152,7 @@ describe('AssetsService', () => { lastModified: '2017-10-12T10:10', lastModifiedBy: 'LastModifiedBy2', fileName: 'My Asset2.png', + fileHash: 'My Hash1', fileType: 'png', fileSize: 1024, fileVersion: 2000, @@ -172,10 +174,12 @@ describe('AssetsService', () => { DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), 'My Asset1.png', + 'My Hash1', 'png', 1024, 2000, 'image/png', + false, true, 1024, 2048, @@ -187,10 +191,12 @@ describe('AssetsService', () => { DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), 'My Asset2.png', + 'My Hash1', 'png', 1024, 2000, 'image/png', + false, true, 1024, 2048, @@ -222,6 +228,7 @@ describe('AssetsService', () => { lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', fileName: 'My Asset1.png', + fileHash: 'My Hash1', fileType: 'png', fileSize: 1024, fileVersion: 2000, @@ -243,10 +250,12 @@ describe('AssetsService', () => { DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), 'My Asset1.png', + 'My Hash1', 'png', 1024, 2000, 'image/png', + false, true, 1024, 2048, @@ -312,10 +321,12 @@ describe('AssetsService', () => { req.flush({ id: 'id1', fileName: 'My Asset1.png', + fileHash: 'My Hash1', fileType: 'png', fileSize: 1024, fileVersion: 2, mimeType: 'image/png', + isDuplicate: true, isImage: true, pixelWidth: 1024, pixelHeight: 2048, @@ -335,10 +346,12 @@ describe('AssetsService', () => { now, now, 'My Asset1.png', + 'My Hash1', 'png', 1024, 2, 'image/png', true, + true, 1024, 2048, 'my-asset1.png', @@ -385,8 +398,9 @@ describe('AssetsService', () => { expect(req.request.headers.get('If-Match')).toEqual(version.value); req.flush({ + fileHash: 'Hash New', fileSize: 1024, - fileVersion: 2, + fileVersion: 12, mimeType: 'image/png', isImage: true, pixelWidth: 1024, @@ -395,7 +409,9 @@ describe('AssetsService', () => { expect(asset!).toEqual( new AssetReplacedDto( - 1024, 2, + 'Hash New', + 1024, + 12, 'image/png', true, 1024, @@ -428,7 +444,7 @@ describe('AssetsService', () => { it('should make put request to annotate asset', inject([AssetsService, HttpTestingController], (assetsService: AssetsService, httpMock: HttpTestingController) => { - const dto = new AnnotateAssetDto('My Asset.pdf', 'my-asset.pdf', ['tag1', 'tag2']); + const dto = { fileName: 'New-Name.png' }; assetsService.putAsset('my-app', '123', dto, version).subscribe(); diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 7fa7d0cb3..eb8b5d3ff 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -18,21 +18,15 @@ import { HTTP, Model, pretifyError, + ResultSet, Types, Version, Versioned } from '@app/framework'; -export class AssetsDto extends Model { - constructor( - public readonly total: number, - public readonly items: AssetDto[] - ) { - super(); - } -} +export class AssetsDto extends ResultSet { } -export class AssetDto extends Model { +export class AssetDto extends Model { public get canPreview() { return this.isImage || (this.mimeType === 'image/svg+xml' && this.fileSize < 100 * 1024); } @@ -44,10 +38,12 @@ export class AssetDto extends Model { public readonly created: DateTime, public readonly lastModified: DateTime, public readonly fileName: string, + public readonly fileHash: string, public readonly fileType: string, public readonly fileSize: number, public readonly fileVersion: number, public readonly mimeType: string, + public readonly isDuplicate: boolean, public readonly isImage: boolean, public readonly pixelWidth: number | null, public readonly pixelHeight: number | null, @@ -59,12 +55,8 @@ export class AssetDto extends Model { super(); } - public with(value: Partial, validOnly = false): AssetDto { - return this.clone(value, validOnly); - } - public update(update: AssetReplacedDto, user: string, version: Version, now?: DateTime): AssetDto { - return this.with({ + return this.clone({ ...update, lastModified: now || DateTime.now(), lastModifiedBy: user, @@ -73,7 +65,7 @@ export class AssetDto extends Model { } public annnotate(update: AnnotateAssetDto, user: string, version: Version, now?: DateTime): AssetDto { - return this.with({ + return this.clone({ ...update, lastModified: now || DateTime.now(), lastModifiedBy: user, @@ -82,25 +74,20 @@ export class AssetDto extends Model { } } -export class AnnotateAssetDto { - constructor( - public readonly fileName: string | null, - public readonly slug: string | null, - public readonly tags: string[] | null - ) { - } +export interface AnnotateAssetDto { + readonly fileName?: string; + readonly slug?: string; + readonly tags?: string[]; } -export class AssetReplacedDto { - constructor( - public readonly fileSize: number, - public readonly fileVersion: number, - public readonly mimeType: string, - public readonly isImage: boolean, - public readonly pixelWidth: number | null, - public readonly pixelHeight: number | null - ) { - } +export interface AssetReplacedDto { + readonly fileHash: string; + readonly fileSize: number; + readonly fileVersion: number; + readonly mimeType: string; + readonly isImage: boolean; + readonly pixelWidth?: number | null; + readonly pixelHeight?: number | null; } @Injectable() @@ -169,10 +156,12 @@ export class AssetsService { DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.lastModified), item.fileName, + item.fileHash, item.fileType, item.fileSize, item.fileVersion, item.mimeType, + false, item.isImage, item.pixelWidth, item.pixelHeight, @@ -212,10 +201,12 @@ export class AssetsService { now, now, response.fileName, + response.fileHash, response.fileType, response.fileSize, response.fileVersion, response.mimeType, + response.isDuplicate, response.isImage, response.pixelWidth, response.pixelHeight, @@ -260,10 +251,12 @@ export class AssetsService { DateTime.parseISO_UTC(body.created), DateTime.parseISO_UTC(body.lastModified), body.fileName, + body.fileHash, body.fileType, body.fileSize, body.fileVersion, body.mimeType, + false, body.isImage, body.pixelWidth, body.pixelHeight, @@ -292,15 +285,7 @@ export class AssetsService { } else if (Types.is(event, HttpResponse)) { const response: any = event.body; - const replaced = new AssetReplacedDto( - response.fileSize, - response.fileVersion, - response.mimeType, - response.isImage, - response.pixelWidth, - response.pixelHeight); - - return new Versioned(new Version(event.headers.get('etag')!), replaced); + return new Versioned(new Version(event.headers.get('etag')!), response); } else { throw 'Invalid'; } diff --git a/src/Squidex/app/shared/services/backups.service.spec.ts b/src/Squidex/app/shared/services/backups.service.spec.ts index c88b0f161..3f2c971a2 100644 --- a/src/Squidex/app/shared/services/backups.service.spec.ts +++ b/src/Squidex/app/shared/services/backups.service.spec.ts @@ -14,8 +14,7 @@ import { BackupDto, BackupsService, DateTime, - RestoreDto, - StartRestoreDto + RestoreDto } from './../'; describe('BackupsService', () => { @@ -170,7 +169,9 @@ describe('BackupsService', () => { it('should make post request to start restore', inject([BackupsService, HttpTestingController], (backupsService: BackupsService, httpMock: HttpTestingController) => { - backupsService.postRestore(new StartRestoreDto('http://url')).subscribe(); + const dto = { url: 'http://url' }; + + backupsService.postRestore(dto).subscribe(); const req = httpMock.expectOne('http://service/p/api/apps/restore'); diff --git a/src/Squidex/app/shared/services/backups.service.ts b/src/Squidex/app/shared/services/backups.service.ts index 198db6775..4a9f0dbea 100644 --- a/src/Squidex/app/shared/services/backups.service.ts +++ b/src/Squidex/app/shared/services/backups.service.ts @@ -19,7 +19,7 @@ import { Types } from '@app/framework'; -export class BackupDto extends Model { +export class BackupDto extends Model { constructor( public readonly id: string, public readonly started: DateTime, @@ -32,7 +32,7 @@ export class BackupDto extends Model { } } -export class RestoreDto { +export class RestoreDto extends Model { constructor( public readonly url: string, public readonly started: DateTime, @@ -40,15 +40,13 @@ export class RestoreDto { public readonly status: string, public readonly log: string[] ) { + super(); } } -export class StartRestoreDto { - constructor( - public readonly url: string, - public readonly newAppName?: string - ) { - } +export interface StartRestoreDto { + readonly url: string; + readonly newAppName?: string; } @Injectable() diff --git a/src/Squidex/app/shared/services/comments.service.spec.ts b/src/Squidex/app/shared/services/comments.service.spec.ts index 9e5b7290b..03be1dcc6 100644 --- a/src/Squidex/app/shared/services/comments.service.spec.ts +++ b/src/Squidex/app/shared/services/comments.service.spec.ts @@ -14,7 +14,6 @@ import { CommentsDto, CommentsService, DateTime, - UpsertCommentDto, Version } from './../'; @@ -84,9 +83,11 @@ describe('CommentsService', () => { it('should make post request to create comment', inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { + const dto = { text: 'text1' }; + let comment: CommentDto; - commentsService.postComment('my-app', 'my-comments', new UpsertCommentDto('text1')).subscribe(result => { + commentsService.postComment('my-app', 'my-comments', dto).subscribe(result => { comment = result; }); @@ -108,7 +109,9 @@ describe('CommentsService', () => { it('should make put request to replace comment content', inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => { - commentsService.putComment('my-app', 'my-comments', '123', new UpsertCommentDto('text1')).subscribe(); + const dto = { text: 'text1' }; + + commentsService.putComment('my-app', 'my-comments', '123', dto).subscribe(); const req = httpMock.expectOne('http://service/p/api/apps/my-app/comments/my-comments/123'); diff --git a/src/Squidex/app/shared/services/comments.service.ts b/src/Squidex/app/shared/services/comments.service.ts index dc2dc762c..b170ccfd0 100644 --- a/src/Squidex/app/shared/services/comments.service.ts +++ b/src/Squidex/app/shared/services/comments.service.ts @@ -18,7 +18,7 @@ import { Version } from '@app/framework'; -export class CommentsDto extends Model { +export class CommentsDto extends Model { constructor( public readonly createdComments: CommentDto[], public readonly updatedComments: CommentDto[], @@ -29,7 +29,7 @@ export class CommentsDto extends Model { } } -export class CommentDto extends Model { +export class CommentDto extends Model { constructor( public readonly id: string, public readonly time: DateTime, @@ -38,17 +38,10 @@ export class CommentDto extends Model { ) { super(); } - - public with(value: Partial): CommentDto { - return this.clone(value); - } } -export class UpsertCommentDto { - constructor( - public readonly text: string - ) { - } +export interface UpsertCommentDto { + readonly text: string; } @Injectable() diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 7fa8358fc..6888dc646 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -17,25 +17,12 @@ import { HTTP, Model, pretifyError, + ResultSet, Version, Versioned } from '@app/framework'; -export class ContentsDto extends Model { - constructor( - public readonly total: number, - public readonly items: ContentDto[] - ) { - - super(); - } - - public with(value: Partial): ContentsDto { - return this.clone(value); - }} - - -export class ScheduleDto extends Model { +export class ScheduleDto extends Model { constructor( public readonly status: string, public readonly scheduledBy: string, @@ -45,7 +32,9 @@ export class ScheduleDto extends Model { } } -export class ContentDto extends Model { +export class ContentsDto extends ResultSet { } + +export class ContentDto extends Model { constructor( public readonly id: string, public readonly status: string, diff --git a/src/Squidex/app/shared/services/plans.service.spec.ts b/src/Squidex/app/shared/services/plans.service.spec.ts index d3139bc1b..35396170d 100644 --- a/src/Squidex/app/shared/services/plans.service.spec.ts +++ b/src/Squidex/app/shared/services/plans.service.spec.ts @@ -11,7 +11,6 @@ import { inject, TestBed } from '@angular/core/testing'; import { AnalyticsService, ApiUrlConfig, - ChangePlanDto, PlanChangedDto, PlanDto, PlansDto, @@ -101,7 +100,7 @@ describe('PlansService', () => { it('should make put request to change plan', inject([PlansService, HttpTestingController], (plansService: PlansService, httpMock: HttpTestingController) => { - const dto = new ChangePlanDto('enterprise'); + const dto = { planId: 'enterprise' }; let planChanged: PlanChangedDto; @@ -116,6 +115,6 @@ describe('PlansService', () => { expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toBe(version.value); - expect(planChanged!).toEqual(new PlanChangedDto('my-url')); + expect(planChanged!).toEqual({ redirectUri: 'http://url' }); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/plans.service.ts b/src/Squidex/app/shared/services/plans.service.ts index 9ab81561f..5298380ee 100644 --- a/src/Squidex/app/shared/services/plans.service.ts +++ b/src/Squidex/app/shared/services/plans.service.ts @@ -20,7 +20,7 @@ import { Versioned } from '@app/framework'; -export class PlansDto extends Model { +export class PlansDto extends Model { constructor( public readonly currentPlanId: string, public readonly planOwner: string, @@ -32,7 +32,7 @@ export class PlansDto extends Model { } } -export class PlanDto extends Model { +export class PlanDto extends Model { constructor( public readonly id: string, public readonly name: string, @@ -47,18 +47,12 @@ export class PlanDto extends Model { } } -export class PlanChangedDto { - constructor( - public readonly redirectUri: string - ) { - } +export interface PlanChangedDto { + readonly redirectUri?: string; } -export class ChangePlanDto { - constructor( - public readonly planId: string - ) { - } +export interface ChangePlanDto { + readonly planId: string; } @Injectable() @@ -106,7 +100,7 @@ export class PlansService { map(response => { const body = response.payload.body; - return new Versioned(response.version, new PlanChangedDto(body.redirectUri)); + return new Versioned(response.version, body); }), tap(() => { this.analytics.trackEvent('Plan', 'Changed', appName); diff --git a/src/Squidex/app/shared/services/rules.service.spec.ts b/src/Squidex/app/shared/services/rules.service.spec.ts index 7005eb894..5a46294ff 100644 --- a/src/Squidex/app/shared/services/rules.service.spec.ts +++ b/src/Squidex/app/shared/services/rules.service.spec.ts @@ -12,7 +12,6 @@ import { inject, TestBed } from '@angular/core/testing'; import { AnalyticsService, ApiUrlConfig, - CreateRuleDto, DateTime, RuleDto, RuleElementDto, @@ -20,7 +19,6 @@ import { RuleEventDto, RuleEventsDto, RulesService, - UpdateRuleDto, Version } from './../'; @@ -168,15 +166,18 @@ describe('RulesService', () => { it('should make post request to create rule', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - const dto = new CreateRuleDto({ - param1: 1, - param2: 2, - triggerType: 'ContentChanged' - }, { - param3: 3, - param4: 4, - actionType: 'Webhook' - }); + const dto = { + trigger: { + param1: 1, + param2: 2, + triggerType: 'ContentChanged' + }, + action: { + param3: 3, + param4: 4, + actionType: 'Webhook' + } + }; let rule: RuleDto; @@ -216,7 +217,14 @@ describe('RulesService', () => { it('should make put request to update rule', inject([RulesService, HttpTestingController], (rulesService: RulesService, httpMock: HttpTestingController) => { - const dto = new UpdateRuleDto({ param1: 1 }, { param2: 2 }); + const dto = { + trigger: { + param1: 1 + }, + action: { + param3: 2 + } + }; rulesService.putRule('my-app', '123', dto, version).subscribe(); diff --git a/src/Squidex/app/shared/services/rules.service.ts b/src/Squidex/app/shared/services/rules.service.ts index 9ca980f73..11b5f6e5a 100644 --- a/src/Squidex/app/shared/services/rules.service.ts +++ b/src/Squidex/app/shared/services/rules.service.ts @@ -17,6 +17,7 @@ import { HTTP, Model, pretifyError, + ResultSet, Version, Versioned } from '@app/framework'; @@ -72,7 +73,7 @@ export class RuleElementPropertyDto { } } -export class RuleDto extends Model { +export class RuleDto extends Model { constructor( public readonly id: string, public readonly createdBy: string, @@ -88,22 +89,11 @@ export class RuleDto extends Model { ) { super(); } - - public with(value: Partial): RuleDto { - return this.clone(value); - } } -export class RuleEventsDto extends Model { - constructor( - public readonly total: number, - public readonly items: RuleEventDto[] - ) { - super(); - } -} +export class RuleEventsDto extends ResultSet { } -export class RuleEventDto extends Model { +export class RuleEventDto extends Model { constructor( public readonly id: string, public readonly created: DateTime, @@ -117,26 +107,16 @@ export class RuleEventDto extends Model { ) { super(); } - - public with(value: Partial): RuleEventDto { - return this.clone(value); - } } -export class CreateRuleDto { - constructor( - public readonly trigger: any, - public readonly action: any - ) { - } +export interface CreateRuleDto { + readonly trigger: any; + readonly action: any; } -export class UpdateRuleDto { - constructor( - public readonly trigger: any, - public readonly action: any - ) { - } +export interface UpdateRuleDto { + readonly trigger?: any; + readonly action?: any; } @Injectable() diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index f354699e7..d6ea001bd 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -22,9 +22,6 @@ import { SchemaDto, SchemaPropertiesDto, SchemasService, - UpdateFieldDto, - UpdateSchemaCategoryDto, - UpdateSchemaDto, Version } from './../'; @@ -358,7 +355,7 @@ describe('SchemasService', () => { it('should make put request to update schema', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = new UpdateSchemaDto('label', 'hints'); + const dto = { label: 'label1' }; schemasService.putSchema('my-app', 'my-schema', dto, version).subscribe(); @@ -388,7 +385,7 @@ describe('SchemasService', () => { it('should make put request to update category', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = new UpdateSchemaCategoryDto(); + const dto = {}; schemasService.putCategory('my-app', 'my-schema', dto, version).subscribe(); @@ -486,7 +483,7 @@ describe('SchemasService', () => { it('should make put request to update field', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = new UpdateFieldDto(createProperties('Number')); + const dto = { properties: createProperties('Number') }; schemasService.putField('my-app', 'my-schema', 1, dto, undefined, version).subscribe(); @@ -501,7 +498,7 @@ describe('SchemasService', () => { it('should make put request to update nested field', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { - const dto = new UpdateFieldDto(createProperties('Number')); + const dto = { properties: createProperties('Number') }; schemasService.putField('my-app', 'my-schema', 1, dto, 13, version).subscribe(); diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 2457dc456..e9248d2eb 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -24,7 +24,7 @@ import { import { createProperties, FieldPropertiesDto } from './schemas.types'; -export class SchemaDto extends Model { +export class SchemaDto extends Model { public get displayName() { return StringHelper.firstNonEmpty(this.properties.label, this.name); } @@ -44,10 +44,6 @@ export class SchemaDto extends Model { ) { super(); } - - public with(value: Partial): SchemaDto { - return this.clone(value); - } } export class SchemaDetailsDto extends SchemaDto { @@ -88,13 +84,9 @@ export class SchemaDetailsDto extends SchemaDto { this.listFieldsEditable = fields.filter(x => x.isInlineEditable); } } - - public with(value: Partial): SchemaDetailsDto { - return this.clone(value); - } } -export class FieldDto extends Model { +export class FieldDto extends Model { public get isInlineEditable(): boolean { return !this.isDisabled && this.properties['inlineEditable'] === true; } @@ -117,10 +109,6 @@ export class FieldDto extends Model { ) { super(); } - - public with(value: Partial): FieldDto { - return this.clone(value); - } } export class RootFieldDto extends FieldDto { @@ -149,10 +137,6 @@ export class RootFieldDto extends FieldDto { ) { super(fieldId, name, properties, isLocked, isHidden, isDisabled); } - - public with(value: Partial): RootFieldDto { - return this.clone(value); - } } export class NestedFieldDto extends FieldDto { @@ -164,10 +148,6 @@ export class NestedFieldDto extends FieldDto { ) { super(fieldId, name, properties, isLocked, isHidden, isDisabled); } - - public with(value: Partial): NestedFieldDto { - return this.clone(value); - } } export class SchemaPropertiesDto { @@ -197,26 +177,17 @@ export class CreateSchemaDto { } } -export class UpdateSchemaCategoryDto { - constructor( - public readonly name?: string - ) { - } +export interface UpdateSchemaCategoryDto { + readonly name?: string; } -export class UpdateFieldDto { - constructor( - public readonly properties: FieldPropertiesDto - ) { - } +export interface UpdateFieldDto { + readonly properties: FieldPropertiesDto; } -export class UpdateSchemaDto { - constructor( - public readonly label?: string, - public readonly hints?: string - ) { - } +export interface UpdateSchemaDto { + readonly label?: string; + readonly hints?: string; } @Injectable() diff --git a/src/Squidex/app/shared/services/schemas.types.ts b/src/Squidex/app/shared/services/schemas.types.ts index ba94c6567..d2238838f 100644 --- a/src/Squidex/app/shared/services/schemas.types.ts +++ b/src/Squidex/app/shared/services/schemas.types.ts @@ -5,7 +5,19 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -export const fieldTypes = [ +export type FieldType = + 'Array' | + 'Assets' | + 'Boolean' | + 'DateTime' | + 'Json' | + 'Geolocation' | + 'Number' | + 'References' | + 'String' | + 'Tags'; + +export const fieldTypes: { type: FieldType, description: string }[] = [ { type: 'String', description: 'Titles, names, paragraphs.' @@ -41,7 +53,7 @@ export const fieldTypes = [ export const fieldInvariant = 'iv'; -export function createProperties(fieldType: string, values: Object | null = null): FieldPropertiesDto { +export function createProperties(fieldType: FieldType, values: Object | null = null): FieldPropertiesDto { let properties: FieldPropertiesDto; switch (fieldType) { @@ -109,7 +121,7 @@ export interface FieldPropertiesVisitor { } export abstract class FieldPropertiesDto { - public abstract fieldType: string; + public abstract fieldType: FieldType; public readonly editorUrl?: string; public readonly label?: string; diff --git a/src/Squidex/app/shared/services/translations.service.spec.ts b/src/Squidex/app/shared/services/translations.service.spec.ts index ac4beed2e..864fab019 100644 --- a/src/Squidex/app/shared/services/translations.service.spec.ts +++ b/src/Squidex/app/shared/services/translations.service.spec.ts @@ -10,7 +10,6 @@ import { inject, TestBed } from '@angular/core/testing'; import { ApiUrlConfig, - TranslateDto, TranslationDto, TranslationsService } from './../'; @@ -35,11 +34,11 @@ describe('TranslationsService', () => { it('should make post request to translate text', inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => { - let translation: TranslationDto; + const dto = { text: 'Hello', sourceLanguage: 'en', targetLanguage: 'de' }; - const request = new TranslateDto('Hello', 'en', 'de'); + let translation: TranslationDto; - translationsService.translate('my-app', request).subscribe(result => { + translationsService.translate('my-app', dto).subscribe(result => { translation = result; }); diff --git a/src/Squidex/app/shared/services/translations.service.ts b/src/Squidex/app/shared/services/translations.service.ts index 618ace20b..b6dd270de 100644 --- a/src/Squidex/app/shared/services/translations.service.ts +++ b/src/Squidex/app/shared/services/translations.service.ts @@ -20,13 +20,10 @@ export class TranslationDto { } } -export class TranslateDto { - constructor( - public readonly text: string, - public readonly sourceLanguage: string, - public readonly targetLanguage: string - ) { - } +export interface TranslateDto { + readonly text: string; + readonly sourceLanguage: string; + readonly targetLanguage: string; } @Injectable() diff --git a/src/Squidex/app/shared/services/ui.service.ts b/src/Squidex/app/shared/services/ui.service.ts index 91ce220ec..97115ccff 100644 --- a/src/Squidex/app/shared/services/ui.service.ts +++ b/src/Squidex/app/shared/services/ui.service.ts @@ -13,10 +13,10 @@ import { catchError } from 'rxjs/operators'; import { ApiUrlConfig } from '@app/framework'; export interface UISettingsDto { - mapType: string; - mapKey?: string; + readonly mapType: string; + readonly mapKey?: string; - canCreateApps: boolean; + readonly canCreateApps: boolean; } @Injectable() diff --git a/src/Squidex/app/shared/state/apps.state.spec.ts b/src/Squidex/app/shared/state/apps.state.spec.ts index fd8c16c49..83eecd9ea 100644 --- a/src/Squidex/app/shared/state/apps.state.spec.ts +++ b/src/Squidex/app/shared/state/apps.state.spec.ts @@ -12,7 +12,6 @@ import { AppDto, AppsService, AppsState, - CreateAppDto, DateTime, DialogService, Permission @@ -22,11 +21,11 @@ describe('AppsState', () => { const now = DateTime.now(); const oldApps = [ - new AppDto('id1', 'old-name1', [new Permission('Owner')], now, now, 'Free', 'Plan'), - new AppDto('id2', 'old-name2', [new Permission('Owner')], now, now, 'Free', 'Plan') + new AppDto('id1', 'old-name1', [new Permission('Owner')], now, now), + new AppDto('id2', 'old-name2', [new Permission('Owner')], now, now) ]; - const newApp = new AppDto('id3', 'new-name', [new Permission('Owner')], now, now, 'Free', 'Plan'); + const newApp = new AppDto('id3', 'new-name', [new Permission('Owner')], now, now); let dialogs: IMock; let appsService: IMock; @@ -83,7 +82,7 @@ describe('AppsState', () => { }); it('should add app to snapshot when created', () => { - const request = new CreateAppDto(newApp.name); + const request = { ...newApp }; appsService.setup(x => x.postApp(request)) .returns(() => of(newApp)); @@ -94,7 +93,7 @@ describe('AppsState', () => { }); it('should remove app from snashot when archived', () => { - const request = new CreateAppDto(newApp.name); + const request = { ...newApp }; appsService.setup(x => x.postApp(request)) .returns(() => of(newApp)); diff --git a/src/Squidex/app/shared/state/assets.state.spec.ts b/src/Squidex/app/shared/state/assets.state.spec.ts index 7b80efe03..aa32d70f3 100644 --- a/src/Squidex/app/shared/state/assets.state.spec.ts +++ b/src/Squidex/app/shared/state/assets.state.spec.ts @@ -30,8 +30,8 @@ describe('AssetsState', () => { const newVersion = new Version('2'); const oldAssets = [ - new AssetDto('id1', creator, creator, creation, creation, 'name1', 'type1', 1, 1, 'mime1', false, null, null, 'slug1', ['tag1', 'shared'], 'url1', version), - new AssetDto('id2', creator, creator, creation, creation, 'name2', 'type2', 2, 2, 'mime2', false, null, null, 'slug2', ['tag2', 'shared'], 'url2', version) + new AssetDto('id1', creator, creator, creation, creation, 'name1', 'hash1', 'type1', 1, 1, 'mime1', false, false, null, null, 'slug1', ['tag1', 'shared'], 'url1', version), + new AssetDto('id2', creator, creator, creation, creation, 'name2', 'hash2', 'type2', 2, 2, 'mime2', false, false, null, null, 'slug2', ['tag2', 'shared'], 'url2', version) ]; let dialogs: IMock; @@ -81,7 +81,7 @@ describe('AssetsState', () => { }); it('should add asset to snapshot when created', () => { - const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'slug2', [], 'url3', version); + const newAsset = new AssetDto('id3', creator, creator, creation, creation, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug2', [], 'url3', version); assetsState.add(newAsset); @@ -90,7 +90,7 @@ describe('AssetsState', () => { }); it('should update properties when updated', () => { - const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'type3', 3, 3, 'mime3', true, 0, 0, 'slug3', ['new'], 'url3', version); + const newAsset = new AssetDto('id1', modifier, modifier, modified, modified, 'name3', 'hash3', 'type3', 3, 3, 'mime3', false, true, 0, 0, 'slug3', ['new'], 'url3', version); assetsState.update(newAsset); diff --git a/src/Squidex/app/shared/state/clients.state.spec.ts b/src/Squidex/app/shared/state/clients.state.spec.ts index 27a1ebcfa..f11706196 100644 --- a/src/Squidex/app/shared/state/clients.state.spec.ts +++ b/src/Squidex/app/shared/state/clients.state.spec.ts @@ -14,9 +14,7 @@ import { AppClientsService, AppsState, ClientsState, - CreateAppClientDto, DialogService, - UpdateAppClientDto, Version, Versioned } from './../'; @@ -27,8 +25,8 @@ describe('ClientsState', () => { const newVersion = new Version('2'); const oldClients = [ - new AppClientDto('id1', 'name1', 'secret1', 'Developer'), - new AppClientDto('id2', 'name2', 'secret2', 'Developer') + new AppClientDto('id1', 'name1', 'secret1'), + new AppClientDto('id2', 'name2', 'secret2') ]; let dialogs: IMock; @@ -70,9 +68,9 @@ describe('ClientsState', () => { }); it('should add client to snapshot when created', () => { - const newClient = new AppClientDto('id3', 'name3', 'secret3', 'Developer'); + const newClient = new AppClientDto('id3', 'name3', 'secret3'); - const request = new CreateAppClientDto('id3'); + const request = { id: 'id3' }; clientsService.setup(x => x.postClient(app, request, version)) .returns(() => of(new Versioned(newVersion, newClient))); @@ -84,7 +82,7 @@ describe('ClientsState', () => { }); it('should update properties when updated', () => { - const request = new UpdateAppClientDto('NewName', 'NewRole'); + const request = { name: 'NewName', role: 'NewRole' }; clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version)) .returns(() => of(new Versioned(newVersion, {}))); diff --git a/src/Squidex/app/shared/state/comments.state.spec.ts b/src/Squidex/app/shared/state/comments.state.spec.ts index 4be327dbe..7a101a793 100644 --- a/src/Squidex/app/shared/state/comments.state.spec.ts +++ b/src/Squidex/app/shared/state/comments.state.spec.ts @@ -17,7 +17,6 @@ import { DateTime, DialogService, ImmutableArray, - UpsertCommentDto, Version } from './../'; @@ -56,10 +55,10 @@ describe('CommentsState', () => { it('should load and merge comments', () => { const newComments = new CommentsDto([ - new CommentDto('3', now, 'text3', user) - ], [ - new CommentDto('2', now, 'text2_2', user) - ], ['1'], new Version('2')); + new CommentDto('3', now, 'text3', user) + ], [ + new CommentDto('2', now, 'text2_2', user) + ], ['1'], new Version('2')); commentsService.setup(x => x.getComments(app, commentsId, new Version('1'))) .returns(() => of(newComments)); @@ -78,7 +77,9 @@ describe('CommentsState', () => { it('should add comment to snapshot when created', () => { const newComment = new CommentDto('3', now, 'text3', user); - commentsService.setup(x => x.postComment(app, commentsId, new UpsertCommentDto('text3'))) + const request = { text: 'text3' }; + + commentsService.setup(x => x.postComment(app, commentsId, request)) .returns(() => of(newComment)); commentsState.create('text3').subscribe(); @@ -91,7 +92,9 @@ describe('CommentsState', () => { }); it('should update properties when updated', () => { - commentsService.setup(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2'))) + const request = { text: 'text2_2' }; + + commentsService.setup(x => x.putComment(app, commentsId, '2', request)) .returns(() => of({})); commentsState.update('2', 'text2_2', now).subscribe(); @@ -101,7 +104,7 @@ describe('CommentsState', () => { new CommentDto('2', now, 'text2_2', user) ])); - commentsService.verify(x => x.putComment(app, commentsId, '2', new UpsertCommentDto('text2_2')), Times.once()); + commentsService.verify(x => x.putComment(app, commentsId, '2', request), Times.once()); }); it('should remove comment from snapshot when deleted', () => { diff --git a/src/Squidex/app/shared/state/comments.state.ts b/src/Squidex/app/shared/state/comments.state.ts index a330ac9ec..fb653ad3e 100644 --- a/src/Squidex/app/shared/state/comments.state.ts +++ b/src/Squidex/app/shared/state/comments.state.ts @@ -17,12 +17,7 @@ import { Version } from '@app/framework'; -import { - CommentDto, - CommentsService, - UpsertCommentDto -} from './../services/comments.service'; - +import { CommentDto, CommentsService } from './../services/comments.service'; import { AppsState } from './apps.state'; interface Snapshot { @@ -81,7 +76,7 @@ export class CommentsState extends State { } public create(text: string): Observable { - return this.commentsService.postComment(this.appName, this.commentsId, new UpsertCommentDto(text)).pipe( + return this.commentsService.postComment(this.appName, this.commentsId, { text }).pipe( tap(dto => { this.next(s => { const comments = s.comments.push(dto); @@ -93,7 +88,7 @@ export class CommentsState extends State { } public update(commentId: string, text: string, now?: DateTime): Observable { - return this.commentsService.putComment(this.appName, this.commentsId, commentId, new UpsertCommentDto(text)).pipe( + return this.commentsService.putComment(this.appName, this.commentsId, commentId, { text }).pipe( tap(() => { this.next(s => { const comments = s.comments.map(c => c.id === commentId ? update(c, text, now || DateTime.now()) : c); diff --git a/src/Squidex/app/shared/state/contributors.state.spec.ts b/src/Squidex/app/shared/state/contributors.state.spec.ts index e3992cad6..0c2893e95 100644 --- a/src/Squidex/app/shared/state/contributors.state.spec.ts +++ b/src/Squidex/app/shared/state/contributors.state.spec.ts @@ -13,7 +13,6 @@ import { AppContributorsDto, AppContributorsService, AppsState, - AssignContributorDto, AuthService, ContributorAssignedDto, ContributorsState, @@ -84,7 +83,7 @@ describe('ContributorsState', () => { it('should add contributor to snapshot when assigned', () => { const newContributor = new AppContributorDto('id3', 'Developer'); - const request = new AssignContributorDto('mail2stehle@gmail.com', 'Developer'); + const request = { contributorId: 'mail2stehle@gmail.com', role: 'Developer' }; contributorsService.setup(x => x.postContributor(app, request, version)) .returns(() => of(new Versioned(newVersion, new ContributorAssignedDto('id3', true)))); diff --git a/src/Squidex/app/shared/state/languages.state.spec.ts b/src/Squidex/app/shared/state/languages.state.spec.ts index 1d4c41382..97ab66264 100644 --- a/src/Squidex/app/shared/state/languages.state.spec.ts +++ b/src/Squidex/app/shared/state/languages.state.spec.ts @@ -18,7 +18,6 @@ import { LanguageDto, LanguagesService, LanguagesState, - UpdateAppLanguageDto, Version, Versioned } from './../'; @@ -121,7 +120,7 @@ describe('LanguagesState', () => { }); it('should update language in snapshot when updated', () => { - const request = new UpdateAppLanguageDto(true, false, []); + const request = { isMaster: true }; languagesService.setup(x => x.putLanguage(app, oldLanguages[1].iso2Code, request, version)) .returns(() => of(new Versioned(newVersion, {}))); diff --git a/src/Squidex/app/shared/state/languages.state.ts b/src/Squidex/app/shared/state/languages.state.ts index 43659f094..9910fe399 100644 --- a/src/Squidex/app/shared/state/languages.state.ts +++ b/src/Squidex/app/shared/state/languages.state.ts @@ -17,7 +17,12 @@ import { Version } from '@app/framework'; -import { AddAppLanguageDto, AppLanguageDto, AppLanguagesService, UpdateAppLanguageDto } from './../services/app-languages.service'; +import { + AppLanguageDto, + AppLanguagesService, + UpdateAppLanguageDto +} from './../services/app-languages.service'; + import { LanguageDto, LanguagesService } from './../services/languages.service'; import { AppsState } from './apps.state'; @@ -105,7 +110,7 @@ export class LanguagesState extends State { } public add(language: LanguageDto): Observable { - return this.appLanguagesService.postLanguage(this.appName, new AddAppLanguageDto(language.iso2Code), this.version).pipe( + return this.appLanguagesService.postLanguage(this.appName, { language: language.iso2Code }, this.version).pipe( tap(dto => { const languages = this.snapshot.plainLanguages.push(dto.payload).sortByStringAsc(x => x.englishName); @@ -129,9 +134,9 @@ export class LanguagesState extends State { tap(dto => { const languages = this.snapshot.plainLanguages.map(l => { if (l.iso2Code === language.iso2Code) { - return update(l, request.isMaster, request.isOptional, request.fallback); + return update(l, request); } else if (l.isMaster && request.isMaster) { - return update(l, false, l.isOptional, l.fallback); + return update(l, { isMaster: false }); } else { return l; } @@ -190,10 +195,5 @@ export class LanguagesState extends State { } } -const update = (language: AppLanguageDto, isMaster: boolean, isOptional: boolean, fallback: string[]) => - new AppLanguageDto( - language.iso2Code, - language.englishName, - isMaster, - isOptional, - fallback); \ No newline at end of file +const update = (language: AppLanguageDto, request: UpdateAppLanguageDto) => + language.with(request); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/patterns.state.spec.ts b/src/Squidex/app/shared/state/patterns.state.spec.ts index 8a72ce685..583bd6ec8 100644 --- a/src/Squidex/app/shared/state/patterns.state.spec.ts +++ b/src/Squidex/app/shared/state/patterns.state.spec.ts @@ -14,7 +14,6 @@ import { AppPatternsService, AppsState, DialogService, - EditAppPatternDto, PatternsState, Version, Versioned @@ -70,7 +69,7 @@ describe('PatternsState', () => { it('should add pattern to snapshot when created', () => { const newPattern = new AppPatternDto('id3', 'name3', 'pattern3', ''); - const request = new EditAppPatternDto('name3', 'pattern3', ''); + const request = { ...newPattern }; patternsService.setup(x => x.postPattern(app, request, version)) .returns(() => of(new Versioned(newVersion, newPattern))); @@ -82,7 +81,7 @@ describe('PatternsState', () => { }); it('should update properties when updated', () => { - const request = new EditAppPatternDto('a_name2', 'a_pattern2', 'a_message2'); + const request = { name: 'name2_1', pattern: 'pattern2_1', message: 'message2_1' }; patternsService.setup(x => x.putPattern(app, oldPatterns[1].id, request, version)) .returns(() => of(new Versioned(newVersion, {}))); @@ -91,9 +90,9 @@ describe('PatternsState', () => { const pattern_1 = patternsState.snapshot.patterns.at(0); - expect(pattern_1.name).toBe('a_name2'); - expect(pattern_1.pattern).toBe('a_pattern2'); - expect(pattern_1.message).toBe('a_message2'); + expect(pattern_1.name).toBe(request.name); + expect(pattern_1.pattern).toBe(request.pattern); + expect(pattern_1.message).toBe(request.message); expect(patternsState.snapshot.version).toEqual(newVersion); }); diff --git a/src/Squidex/app/shared/state/patterns.state.ts b/src/Squidex/app/shared/state/patterns.state.ts index 649308978..7673b50b8 100644 --- a/src/Squidex/app/shared/state/patterns.state.ts +++ b/src/Squidex/app/shared/state/patterns.state.ts @@ -120,4 +120,4 @@ export class PatternsState extends State { } const update = (pattern: AppPatternDto, request: EditAppPatternDto) => - new AppPatternDto(pattern.id, request.name, request.pattern, request.message); \ No newline at end of file + pattern.with(request); \ No newline at end of file diff --git a/src/Squidex/app/shared/state/plans.state.spec.ts b/src/Squidex/app/shared/state/plans.state.spec.ts index ba8929185..e30c52238 100644 --- a/src/Squidex/app/shared/state/plans.state.spec.ts +++ b/src/Squidex/app/shared/state/plans.state.spec.ts @@ -103,7 +103,7 @@ describe('PlansState', () => { plansState.window = { location: {} }; plansService.setup(x => x.putPlan(app, It.isAny(), version)) - .returns(() => of(new Versioned(newVersion, new PlanChangedDto('URI')))); + .returns(() => of(new Versioned(newVersion, { redirectUri: 'http://url' }))); plansState.load().subscribe(); plansState.change('free').pipe(onErrorResumeNext()).subscribe(); @@ -120,7 +120,7 @@ describe('PlansState', () => { plansState.window = { location: {} }; plansService.setup(x => x.putPlan(app, It.isAny(), version)) - .returns(() => of(new Versioned(newVersion, new PlanChangedDto('')))); + .returns(() => of(new Versioned(newVersion, { redirectUri: '' }))); plansState.load().subscribe(); plansState.change('id2_yearly').pipe(onErrorResumeNext()).subscribe(); diff --git a/src/Squidex/app/shared/state/plans.state.ts b/src/Squidex/app/shared/state/plans.state.ts index b4ae83ae3..a0745f2aa 100644 --- a/src/Squidex/app/shared/state/plans.state.ts +++ b/src/Squidex/app/shared/state/plans.state.ts @@ -18,14 +18,9 @@ import { } from '@app/framework'; import { AuthService } from './../services/auth.service'; +import { PlanDto, PlansService } from './../services/plans.service'; import { AppsState } from './apps.state'; -import { - ChangePlanDto, - PlanDto, - PlansService -} from './../services/plans.service'; - interface PlanInfo { // The plan. plan: PlanDto; @@ -116,7 +111,7 @@ export class PlansState extends State { } public change(planId: string): Observable { - return this.plansService.putPlan(this.appName, new ChangePlanDto(planId), this.version).pipe( + return this.plansService.putPlan(this.appName, { planId }, this.version).pipe( tap(dto => { if (dto.payload.redirectUri && dto.payload.redirectUri.length > 0) { this.window.location.href = dto.payload.redirectUri; diff --git a/src/Squidex/app/shared/state/roles.state.spec.ts b/src/Squidex/app/shared/state/roles.state.spec.ts index a52781fcc..eb8c5f59b 100644 --- a/src/Squidex/app/shared/state/roles.state.spec.ts +++ b/src/Squidex/app/shared/state/roles.state.spec.ts @@ -13,10 +13,8 @@ import { AppRolesDto, AppRolesService, AppsState, - CreateAppRoleDto, DialogService, RolesState, - UpdateAppRoleDto, Version, Versioned } from './../'; @@ -72,7 +70,7 @@ describe('RolesState', () => { it('should add role to snapshot when added', () => { const newRole = new AppRoleDto('Role3', 0, 0, ['P3']); - const request = new CreateAppRoleDto('Role3'); + const request = { name: newRole.name }; rolesService.setup(x => x.postRole(app, request, version)) .returns(() => of(new Versioned(newVersion, newRole))); @@ -84,7 +82,7 @@ describe('RolesState', () => { }); it('should update permissions when updated', () => { - const request = new UpdateAppRoleDto(['P4', 'P5']); + const request = { permissions: ['P4', 'P5'] }; rolesService.setup(x => x.putRole(app, oldRoles[1].name, request, version)) .returns(() => of(new Versioned(newVersion, {}))); diff --git a/src/Squidex/app/shared/state/rules.state.spec.ts b/src/Squidex/app/shared/state/rules.state.spec.ts index 59331b2c5..5181981ab 100644 --- a/src/Squidex/app/shared/state/rules.state.spec.ts +++ b/src/Squidex/app/shared/state/rules.state.spec.ts @@ -13,7 +13,6 @@ import { RulesState } from './rules.state'; import { AppsState, AuthService, - CreateRuleDto, DateTime, DialogService, RuleDto, @@ -83,7 +82,7 @@ describe('RulesState', () => { it('should add rule to snapshot when created', () => { const newRule = new RuleDto('id3', creator, creator, creation, creation, version, false, {}, 'trigger3', {}, 'action3'); - const request = new CreateRuleDto({}, {}); + const request = { action: {}, trigger: {} }; rulesService.setup(x => x.postRule(app, request, modifier, creation)) .returns(() => of(newRule)); diff --git a/src/Squidex/app/shared/state/rules.state.ts b/src/Squidex/app/shared/state/rules.state.ts index a8cc1dffb..6cc19956a 100644 --- a/src/Squidex/app/shared/state/rules.state.ts +++ b/src/Squidex/app/shared/state/rules.state.ts @@ -24,8 +24,7 @@ import { AppsState } from './apps.state'; import { CreateRuleDto, RuleDto, - RulesService, - UpdateRuleDto + RulesService } from './../services/rules.service'; interface Snapshot { @@ -100,7 +99,7 @@ export class RulesState extends State { } public updateAction(rule: RuleDto, action: any, now?: DateTime): Observable { - return this.rulesService.putRule(this.appName, rule.id, new UpdateRuleDto(null, action), rule.version).pipe( + return this.rulesService.putRule(this.appName, rule.id, { action }, rule.version).pipe( tap(dto => { this.replaceRule(updateAction(rule, action, this.user, dto.version, now)); }), @@ -108,7 +107,7 @@ export class RulesState extends State { } public updateTrigger(rule: RuleDto, trigger: any, now?: DateTime): Observable { - return this.rulesService.putRule(this.appName, rule.id, new UpdateRuleDto(trigger, null), rule.version).pipe( + return this.rulesService.putRule(this.appName, rule.id, { trigger }, rule.version).pipe( tap(dto => { this.replaceRule(updateTrigger(rule, trigger, this.user, dto.version, now)); }), diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index b0cd01ea5..995f42271 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent public string FileName { get; set; } + public string FileHash { get; set; } + public string Slug { get; set; } public long FileSize { get; set; } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 1d9ac1cb4..e21102546 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -14,6 +14,7 @@ using FakeItEasy; using Orleans; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -26,8 +27,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class AssetCommandMiddlewareTests : HandlerTestBase { + private readonly AssetQueryService assetQueryService = A.Fake(); private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); - private readonly IAssetStore assetStore = A.Fake(); + private readonly IAssetStore assetStore = A.Fake(); private readonly ITagService tagService = A.Fake(); private readonly ITagGenerator tagGenerator = A.Fake>(); private readonly IGrainFactory grainFactory = A.Fake(); @@ -56,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => grainFactory.GetGrain(Id, null)) .Returns(asset); - sut = new AssetCommandMiddleware(grainFactory, assetStore, assetThumbnailGenerator, new[] { tagGenerator }); + sut = new AssetCommandMiddleware(grainFactory, assetQueryService, assetStore, assetThumbnailGenerator, new[] { tagGenerator }); } [Fact] @@ -65,20 +67,14 @@ namespace Squidex.Domain.Apps.Entities.Assets var command = new CreateAsset { AssetId = assetId, File = file }; var context = CreateContextForCommand(command); - A.CallTo(() => tagGenerator.GenerateTags(command, A>.Ignored)) - .Invokes(new Action>((c, tags) => - { - tags.Add("tag1"); - tags.Add("tag2"); - })); - + SetupTags(command); SetupImageInfo(); await sut.HandleAsync(context); var result = context.Result(); - Assert.Equal(assetId, result.Id); + Assert.Equal(assetId, result.IdOrValue); Assert.Contains("tag1", result.Tags); Assert.Contains("tag2", result.Tags); @@ -86,10 +82,66 @@ namespace Squidex.Domain.Apps.Entities.Assets AssertAssetImageChecked(); } + [Fact] + public async Task Create_should_calculate_hash() + { + var command = new CreateAsset { AssetId = assetId, File = file }; + var context = CreateContextForCommand(command); + + SetupImageInfo(); + + await sut.HandleAsync(context); + + Assert.True(command.FileHash.Length > 10); + } + + [Fact] + public async Task Create_should_return_duplicate_result_if_file_with_same_hash_found() + { + var command = new CreateAsset { AssetId = assetId, File = file }; + var context = CreateContextForCommand(command); + + SetupSameHashAsset(file.FileName, file.FileSize, out _); + SetupImageInfo(); + + await sut.HandleAsync(context); + + Assert.True(context.Result().IsDuplicate); + } + + [Fact] + public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found() + { + var command = new CreateAsset { AssetId = assetId, File = file }; + var context = CreateContextForCommand(command); + + SetupSameHashAsset("other-name", file.FileSize, out _); + SetupImageInfo(); + + await sut.HandleAsync(context); + + Assert.False(context.Result().IsDuplicate); + } + + [Fact] + public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found() + { + var command = new CreateAsset { AssetId = assetId, File = file }; + var context = CreateContextForCommand(command); + + SetupSameHashAsset(file.FileName, 12345, out _); + SetupImageInfo(); + + await sut.HandleAsync(context); + + Assert.False(context.Result().IsDuplicate); + } + [Fact] public async Task Update_should_update_domain_object() { - var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file }); + var command = new UpdateAsset { AssetId = assetId, File = file }; + var context = CreateContextForCommand(command); SetupImageInfo(); @@ -101,16 +153,41 @@ namespace Squidex.Domain.Apps.Entities.Assets AssertAssetImageChecked(); } + [Fact] + public async Task Update_should_calculate_hash() + { + var command = new UpdateAsset { AssetId = assetId, File = file }; + var context = CreateContextForCommand(command); + + SetupImageInfo(); + + await ExecuteCreateAsync(); + + await sut.HandleAsync(context); + + Assert.True(command.FileHash.Length > 10); + } + private Task ExecuteCreateAsync() { return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file })); } + private void SetupTags(CreateAsset command) + { + A.CallTo(() => tagGenerator.GenerateTags(command, A>.Ignored)) + .Invokes(new Action>((c, tags) => + { + tags.Add("tag1"); + tags.Add("tag2"); + })); + } + private void AssertAssetHasBeenUploaded(long version, Guid commitId) { var fileName = AssetStoreExtensions.GetFileName(assetId.ToString(), version); - A.CallTo(() => assetStore.UploadAsync(commitId.ToString(), stream, false, CancellationToken.None)) + A.CallTo(() => assetStore.UploadAsync(commitId.ToString(), A.Ignored, false, CancellationToken.None)) .MustHaveHappened(); A.CallTo(() => assetStore.CopyAsync(commitId.ToString(), fileName, CancellationToken.None)) .MustHaveHappened(); @@ -118,6 +195,17 @@ namespace Squidex.Domain.Apps.Entities.Assets .MustHaveHappened(); } + private void SetupSameHashAsset(string fileName, long fileSize, out IAssetEntity existing) + { + var temp = existing = A.Fake(); + + A.CallTo(() => temp.FileName).Returns(fileName); + A.CallTo(() => temp.FileSize).Returns(fileSize); + + A.CallTo(() => assetQueryService.FindAssetByHashAsync(A.Ignored, A.Ignored)) + .Returns(existing); + } + private void SetupImageInfo() { A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs index 1846de4b4..8ef290b23 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs @@ -27,8 +27,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { private readonly ITagService tagService = A.Fake(); private readonly ImageInfo image = new ImageInfo(2048, 2048); - private readonly Guid assetId = Guid.NewGuid(); private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); + private readonly Guid assetId = Guid.NewGuid(); + private readonly string fileHash = Guid.NewGuid().ToString(); private readonly AssetGrain sut; protected override Guid Id @@ -57,13 +58,14 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Create_should_create_events() { - var command = new CreateAsset { File = file, ImageInfo = image }; + var command = new CreateAsset { File = file, ImageInfo = image, FileHash = fileHash, Tags = new HashSet() }; var result = await sut.ExecuteAsync(CreateAssetCommand(command)); - result.ShouldBeEquivalent(new AssetSavedResult(0, 0)); + result.ShouldBeEquivalent(new AssetSavedResult(0, 0, fileHash)); Assert.Equal(0, sut.Snapshot.FileVersion); + Assert.Equal(fileHash, sut.Snapshot.FileHash); LastEvents .ShouldHaveSameEvents( @@ -71,6 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { IsImage = true, FileName = file.FileName, + FileHash = fileHash, FileSize = file.FileSize, FileVersion = 0, MimeType = file.MimeType, @@ -85,15 +88,16 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Update_should_create_events() { - var command = new UpdateAsset { File = file, ImageInfo = image }; + var command = new UpdateAsset { File = file, ImageInfo = image, FileHash = fileHash }; await ExecuteCreateAsync(); var result = await sut.ExecuteAsync(CreateAssetCommand(command)); - result.ShouldBeEquivalent(new AssetSavedResult(1, 1)); + result.ShouldBeEquivalent(new AssetSavedResult(1, 1, fileHash)); Assert.Equal(1, sut.Snapshot.FileVersion); + Assert.Equal(fileHash, sut.Snapshot.FileHash); LastEvents .ShouldHaveSameEvents( @@ -101,6 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { IsImage = true, FileSize = file.FileSize, + FileHash = fileHash, FileVersion = 1, MimeType = file.MimeType, PixelWidth = image.PixelWidth, diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs index f5520618b..308faf158 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetQueryServiceTests.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var id = Guid.NewGuid(); - A.CallTo(() => assetRepository.FindAssetAsync(id)) + A.CallTo(() => assetRepository.FindAssetAsync(id, false)) .Returns(CreateAsset(id, "id1", "id2", "id3")); var result = await sut.FindAssetAsync(context, id); @@ -65,6 +65,19 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags); } + [Fact] + public async Task Should_find_asset_by_hash_and_resolve_tags() + { + var id = Guid.NewGuid(); + + A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash")) + .Returns(CreateAsset(id, "id1", "id2", "id3")); + + var result = await sut.FindAssetByHashAsync(appId.Id, "hash"); + + Assert.Equal(HashSet.Of("name1", "name2", "name3"), result.Tags); + } + [Fact] public async Task Should_load_assets_from_ids_and_resolve_tags() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs index d51659fca..2daf028de 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs @@ -37,6 +37,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData public string FileName { get; set; } + public string FileHash { get; set; } + public string Slug { get; set; } public long FileSize { get; set; } diff --git a/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs new file mode 100644 index 000000000..fd8e07631 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Assets/HasherStreamTests.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Security.Cryptography; +using Xunit; + +namespace Squidex.Infrastructure.Assets +{ + public class HasherStreamTests + { + [Fact] + public void Should_calculate_hash_while_copying() + { + var source = GenerateTestData(); + var sourceHash = source.Sha256Base64(); + + var sourceStream = new HasherStream(new MemoryStream(source), HashAlgorithmName.SHA256); + + using (sourceStream) + { + var target = new MemoryStream(); + + sourceStream.CopyTo(target); + + var targetHash = sourceStream.GetHashStringAndReset(); + + Assert.Equal(sourceHash, targetHash); + } + } + + private byte[] GenerateTestData(int length = 1000) + { + var random = new Random(); + var result = new byte[length]; + + random.NextBytes(result); + + return result; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs b/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs index a7ea52049..cca1080fa 100644 --- a/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/DispatchingTests.cs @@ -10,6 +10,8 @@ using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Tasks; using Xunit; +#pragma warning disable IDE0060 // Remove unused parameter + namespace Squidex.Infrastructure { public class DispatchingTests @@ -95,13 +97,13 @@ namespace Squidex.Infrastructure public Task On(MyEventA @event, int context) { - EventATriggered = EventATriggered + context; + EventATriggered += context; return TaskHelper.Done; } public Task On(MyEventB @event, int context) { - EventBTriggered = EventATriggered + context; + EventBTriggered += context; return TaskHelper.Done; } } @@ -169,12 +171,12 @@ namespace Squidex.Infrastructure public void On(MyEventA @event, int context) { - EventATriggered = EventATriggered + context; + EventATriggered += context; } public void On(MyEventB @event, int context) { - EventBTriggered = EventATriggered + context; + EventBTriggered += context; } }