diff --git a/src/Squidex.Events/Assets/AssetCreated.cs b/src/Squidex.Events/Assets/AssetCreated.cs index b673adb28..890dec4f1 100644 --- a/src/Squidex.Events/Assets/AssetCreated.cs +++ b/src/Squidex.Events/Assets/AssetCreated.cs @@ -13,12 +13,16 @@ namespace Squidex.Events.Assets [TypeName("AssetCreatedEvent")] public class AssetCreated : AssetEvent { - public string Name { get; set; } + public string FileName { get; set; } public string MimeType { get; set; } public long FileSize { get; set; } public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } } } diff --git a/src/Squidex.Events/Assets/AssetRenamed.cs b/src/Squidex.Events/Assets/AssetRenamed.cs index 8eaea32f0..89c23f627 100644 --- a/src/Squidex.Events/Assets/AssetRenamed.cs +++ b/src/Squidex.Events/Assets/AssetRenamed.cs @@ -13,6 +13,6 @@ namespace Squidex.Events.Assets [TypeName("AssetRenamedEvent")] public class AssetRenamed : AssetEvent { - public string Name { get; set; } + public string FileName { get; set; } } } diff --git a/src/Squidex.Events/Assets/AssetUpdated.cs b/src/Squidex.Events/Assets/AssetUpdated.cs index 4e2bf8677..b56b22875 100644 --- a/src/Squidex.Events/Assets/AssetUpdated.cs +++ b/src/Squidex.Events/Assets/AssetUpdated.cs @@ -18,5 +18,9 @@ namespace Squidex.Events.Assets public long FileSize { get; set; } public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } } } diff --git a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs index f4bede163..c15226c63 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs @@ -13,7 +13,7 @@ namespace Squidex.Infrastructure.Assets { public interface IAssetThumbnailGenerator { - Task IsValidImageAsync(Stream input); + Task GetImageInfoAsync(Stream input); Task GetThumbnailOrNullAsync(Stream input, int dimension); } diff --git a/src/Squidex.Infrastructure/Assets/ImageInfo.cs b/src/Squidex.Infrastructure/Assets/ImageInfo.cs new file mode 100644 index 000000000..36bd06b53 --- /dev/null +++ b/src/Squidex.Infrastructure/Assets/ImageInfo.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// ImageInfo.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.Assets +{ + public sealed class ImageInfo + { + public int PixelWidth { get; } + + public int PixelHeight { get; } + + public ImageInfo(int pixelWidth, int pixelHeight) + { + PixelWidth = pixelWidth; + PixelHeight = pixelHeight; + } + } +} diff --git a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs index fb550abca..ab60f7b2f 100644 --- a/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs +++ b/src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.Assets.ImageSharp }); } - public Task IsValidImageAsync(Stream input) + public Task GetImageInfoAsync(Stream input) { return Task.Run(() => { @@ -51,11 +51,18 @@ namespace Squidex.Infrastructure.Assets.ImageSharp { var image = new Image(input); - return image.Width > 0 && image.Height > 0; + if (image.Width > 0 && image.Height > 0) + { + return new ImageInfo(image.Width, image.Height); + } + else + { + return null; + } } catch { - return false; + return null; } }); } diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetEntity.cs index 69584033f..bb86cc63a 100644 --- a/src/Squidex.Read.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetEntity.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using MongoDB.Bson.Serialization.Attributes; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -16,16 +14,28 @@ namespace Squidex.Read.MongoDb.Assets [BsonRequired] [BsonElement] - public string Name { get; set; } + public string FileName { get; set; } [BsonRequired] [BsonElement] public long FileSize { get; set; } + [BsonRequired] + [BsonElement] + public bool IsImage { get; set; } + [BsonRequired] [BsonElement] public long Version { get; set; } + [BsonRequired] + [BsonElement] + public int? PixelWidth { get; set; } + + [BsonRequired] + [BsonElement] + public int? PixelHeight { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs index ebd1755c8..37c1493ab 100644 --- a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs @@ -12,13 +12,14 @@ using System.Linq; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; using Squidex.Read.Assets; using Squidex.Read.Assets.Repositories; namespace Squidex.Read.MongoDb.Assets { - public partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository + public partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, IEventConsumer { public MongoAssetRepository(IMongoDatabase database) : base(database) @@ -53,7 +54,7 @@ namespace Squidex.Read.MongoDb.Assets return entity; } - private static FilterDefinition CreateFilter(Guid appId, HashSet mimeTypes, string query) + private static FilterDefinition CreateFilter(Guid appId, ICollection mimeTypes, string query) { var filters = new List> { @@ -67,7 +68,7 @@ namespace Squidex.Read.MongoDb.Assets if (!string.IsNullOrWhiteSpace(query)) { - filters.Add(Filter.Regex(x => x.Name, new BsonRegularExpression(query, "i"))); + filters.Add(Filter.Regex(x => x.FileName, new BsonRegularExpression(query, "i"))); } var filter = Filter.And(filters); diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs index ec2c5acc3..3441aa6da 100644 --- a/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs @@ -1,8 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Text; +// ========================================================================== +// MongoAssetRepository_EventHandling.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + using System.Threading.Tasks; -using Squidex.Events.Apps; using Squidex.Events.Assets; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; @@ -13,6 +17,16 @@ namespace Squidex.Read.MongoDb.Assets { public partial class MongoAssetRepository { + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^asset-"; } + } + public Task On(Envelope @event) { return this.DispatchActionAsync(@event.Payload, @event.Headers); diff --git a/src/Squidex.Read/Assets/IAssetEntity.cs b/src/Squidex.Read/Assets/IAssetEntity.cs index 0dd5a9260..8a352d316 100644 --- a/src/Squidex.Read/Assets/IAssetEntity.cs +++ b/src/Squidex.Read/Assets/IAssetEntity.cs @@ -12,8 +12,14 @@ namespace Squidex.Read.Assets { string MimeType { get; } - string Name { get; } + string FileName { get; } long FileSize { get; } + + bool IsImage { get; } + + int? PixelWidth { get; } + + int? PixelHeight { get; } } } diff --git a/src/Squidex.Write/Assets/AssetCommandHandler.cs b/src/Squidex.Write/Assets/AssetCommandHandler.cs new file mode 100644 index 000000000..60d1aa496 --- /dev/null +++ b/src/Squidex.Write/Assets/AssetCommandHandler.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// AssetCommandHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; +using Squidex.Write.Assets.Commands; + +namespace Squidex.Write.Assets +{ + public class AssetCommandHandler : ICommandHandler + { + private readonly IAggregateHandler handler; + + public AssetCommandHandler(IAggregateHandler handler) + { + Guard.NotNull(handler, nameof(handler)); + + this.handler = handler; + } + + protected async Task On(CreateAsset command, CommandContext context) + { + await handler.CreateAsync(context, c => + { + c.Create(command); + + context.Succeed(EntityCreatedResult.Create(c.Id, c.Version)); + }); + } + + protected async Task On(RenameAsset command, CommandContext context) + { + await handler.UpdateAsync(context, c => c.Rename(command)); + } + + protected async Task On(UpdateAsset command, CommandContext context) + { + await handler.UpdateAsync(context, c => c.Update(command)); + } + + protected Task On(DeleteAsset command, CommandContext context) + { + return handler.UpdateAsync(context, c => c.Delete(command)); + } + + public Task HandleAsync(CommandContext context) + { + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); + } + } +} diff --git a/src/Squidex.Write/Assets/AssetDomainObject.cs b/src/Squidex.Write/Assets/AssetDomainObject.cs index 7fec488ea..eca149e2f 100644 --- a/src/Squidex.Write/Assets/AssetDomainObject.cs +++ b/src/Squidex.Write/Assets/AssetDomainObject.cs @@ -41,12 +41,12 @@ namespace Squidex.Write.Assets protected void On(AssetCreated @event) { - fileName = @event.Name; + fileName = @event.FileName; } protected void On(AssetRenamed @event) { - fileName = @event.Name; + fileName = @event.FileName; } protected void On(AssetDeleted @event) @@ -60,7 +60,7 @@ namespace Squidex.Write.Assets VerifyNotCreated(); - RaiseEvent(SimpleMapper.Map(command, new AssetCreated { Name = command.FileName })); + RaiseEvent(SimpleMapper.Map(command, new AssetCreated())); return this; } @@ -76,12 +76,23 @@ namespace Squidex.Write.Assets return this; } - public AssetDomainObject Rename(RenameAsset command) + public AssetDomainObject Update(UpdateAsset command) { Guard.NotNull(command, nameof(command)); VerifyCreatedAndNotDeleted(); - VerifyDifferentNames(command.Name, () => "Cannot rename asset."); + + RaiseEvent(SimpleMapper.Map(command, new AssetUpdated())); + + return this; + } + + public AssetDomainObject Rename(RenameAsset command) + { + Guard.Valid(command, nameof(command), () => "Cannot rename asset."); + + VerifyCreatedAndNotDeleted(); + VerifyDifferentNames(command.FileName, () => "Cannot rename asset."); RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); @@ -106,7 +117,7 @@ namespace Squidex.Write.Assets private void VerifyCreatedAndNotDeleted() { - if (isDeleted || !string.IsNullOrWhiteSpace(fileName)) + if (isDeleted || string.IsNullOrWhiteSpace(fileName)) { throw new DomainException("Asset has already been deleted or not created yet."); } diff --git a/src/Squidex.Write/Assets/Commands/CreateAsset.cs b/src/Squidex.Write/Assets/Commands/CreateAsset.cs index ef6977e30..501502b14 100644 --- a/src/Squidex.Write/Assets/Commands/CreateAsset.cs +++ b/src/Squidex.Write/Assets/Commands/CreateAsset.cs @@ -17,5 +17,9 @@ namespace Squidex.Write.Assets.Commands public long FileSize { get; set; } public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } } } diff --git a/src/Squidex.Write/Assets/Commands/RenameAsset.cs b/src/Squidex.Write/Assets/Commands/RenameAsset.cs index 078a8fc31..cc76043ff 100644 --- a/src/Squidex.Write/Assets/Commands/RenameAsset.cs +++ b/src/Squidex.Write/Assets/Commands/RenameAsset.cs @@ -13,13 +13,13 @@ namespace Squidex.Write.Assets.Commands { public sealed class RenameAsset : AssetAggregateCommand, IValidatable { - public string Name { get; set; } + public string FileName { get; set; } public void Validate(IList errors) { - if (!string.IsNullOrWhiteSpace(Name)) + if (string.IsNullOrWhiteSpace(FileName)) { - errors.Add(new ValidationError("Name must not be null or empty.", nameof(Name))); + errors.Add(new ValidationError("File name must not be null or empty.", nameof(FileName))); } } } diff --git a/src/Squidex.Write/Assets/Commands/UpdateAsset.cs b/src/Squidex.Write/Assets/Commands/UpdateAsset.cs new file mode 100644 index 000000000..754b8ca0d --- /dev/null +++ b/src/Squidex.Write/Assets/Commands/UpdateAsset.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// UpdateAssetCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Write.Assets.Commands +{ + public class UpdateAsset : AssetAggregateCommand + { + public string MimeType { get; set; } + + public long FileSize { get; set; } + + public bool IsImage { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + } +} diff --git a/src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0 b/src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0 new file mode 100644 index 000000000..ebcf056cc Binary files /dev/null and b/src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0 differ diff --git a/src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0 b/src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0 new file mode 100644 index 000000000..ebcf056cc Binary files /dev/null and b/src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0 differ diff --git a/src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0 b/src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0 new file mode 100644 index 000000000..ebcf056cc Binary files /dev/null and b/src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0 differ diff --git a/src/Squidex/Config/Domain/InfrastructureModule.cs b/src/Squidex/Config/Domain/InfrastructureModule.cs index 0c024ae3f..5a47fa619 100644 --- a/src/Squidex/Config/Domain/InfrastructureModule.cs +++ b/src/Squidex/Config/Domain/InfrastructureModule.cs @@ -18,6 +18,8 @@ using NodaTime; using Squidex.Core.Schemas; using Squidex.Core.Schemas.Json; using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Assets.ImageSharp; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Events; @@ -122,6 +124,10 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); + builder.RegisterType() .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index ed62559b1..fac1918fb 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -18,9 +18,11 @@ using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Services.Implementations; +using Squidex.Read.Assets.Repositories; using Squidex.Read.Contents.Repositories; using Squidex.Read.History.Repositories; using Squidex.Read.MongoDb.Apps; +using Squidex.Read.MongoDb.Assets; using Squidex.Read.MongoDb.Contents; using Squidex.Read.MongoDb.History; using Squidex.Read.MongoDb.Infrastructure; @@ -123,29 +125,37 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() + .As() .As() .As() .AsSelf() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() + .As() + .As() .As() .AsSelf() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() + .As() .As() .As() .AsSelf() .SingleInstance(); + builder.RegisterType() + .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) + .As() + .As() + .AsSelf() + .SingleInstance(); + builder.Register(c => new CompoundEventConsumer( c.Resolve(), diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index 3aa01c5f7..eaff8f4fd 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -63,6 +63,10 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); + builder.RegisterType() .As() .SingleInstance(); diff --git a/src/Squidex/Controllers/Api/Assets/AssetsController.cs b/src/Squidex/Controllers/Api/Assets/AssetController.cs similarity index 53% rename from src/Squidex/Controllers/Api/Assets/AssetsController.cs rename to src/Squidex/Controllers/Api/Assets/AssetController.cs index ca6f5869e..ff7c734d4 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetsController.cs +++ b/src/Squidex/Controllers/Api/Assets/AssetController.cs @@ -1,5 +1,5 @@ // ========================================================================== -// AssetsController.cs +// AssetController.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,20 +7,27 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSwag.Annotations; +using Squidex.Controllers.Api.Assets.Models; using Squidex.Core.Identity; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; +using Squidex.Read.Assets.Repositories; using Squidex.Write.Assets.Commands; +#pragma warning disable 1573 + namespace Squidex.Controllers.Api.Assets { /// @@ -30,41 +37,87 @@ namespace Squidex.Controllers.Api.Assets [ApiExceptionFilter] [ServiceFilter(typeof(AppFilterAttribute))] [SwaggerTag("Assets")] - public class AssetsController : ControllerBase + public class AssetController : ControllerBase { private readonly IAssetStore assetStorage; + private readonly IAssetRepository assetRepository; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly AssetConfig assetsConfig; - public AssetsController( + public AssetController( ICommandBus commandBus, - IAssetStore assetStorage, + IAssetStore assetStorage, + IAssetRepository assetRepository, IAssetThumbnailGenerator assetThumbnailGenerator, IOptions assetsConfig) : base(commandBus) { this.assetStorage = assetStorage; - this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetsConfig = assetsConfig.Value; + this.assetRepository = assetRepository; + this.assetThumbnailGenerator = assetThumbnailGenerator; + } + + /// + /// Get assets. + /// + /// + /// 200 => assets returned. + /// + [HttpGet] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetsDto), 200)] + public async Task GetAssets([FromQuery] string query = null, [FromQuery] string mimeTypes = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) + { + var mimeTypeList = new HashSet(); + + if (!string.IsNullOrWhiteSpace(mimeTypes)) + { + foreach (var mimeType in mimeTypes.Split(',')) + { + mimeTypeList.Add(mimeType.Trim()); + } + } + + var taskForAssets = assetRepository.QueryAsync(AppId, mimeTypeList, query, take, skip); + var taskForCount = assetRepository.CountAsync(AppId, mimeTypeList, query); + + await Task.WhenAll(taskForAssets, taskForCount); + + var model = new AssetsDto + { + Total = taskForCount.Result, + Items = taskForAssets.Result.Select(x => SimpleMapper.Map(x, new AssetDto())).ToArray() + }; + + return Ok(model); } /// /// Creates and uploads a new asset. /// /// The name of the app. - /// The name of the schema. /// /// 201 => Asset created. /// 404 => App not found. /// 400 => Asset exceeds the maximum size. /// [HttpPost] - [Route("apps/{app}/schemas/{name}/fields/")] - [ProducesResponseType(typeof(EntityCreatedDto), 201)] + [Route("apps/{app}/assets/")] + [ProducesResponseType(typeof(AssetDto), 201)] [ProducesResponseType(typeof(ErrorDto), 409)] [ProducesResponseType(typeof(ErrorDto), 400)] - public async Task PostAsset(string app, IFormFile file) + public async Task PostAsset(string app, List files) { + if (files.Count != 1) + { + var error = new ValidationError($"Can only upload one file, found ${files.Count}."); + + throw new ValidationException("Cannot create asset.", error); + } + + var file = files[0]; + if (file.Length > assetsConfig.MaxSize) { var error = new ValidationError($"File size cannot be longer than ${assetsConfig.MaxSize}."); @@ -78,13 +131,17 @@ namespace Squidex.Controllers.Api.Assets fileContent.Position = 0; + var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(fileContent); + var command = new CreateAsset { AssetId = Guid.NewGuid(), FileSize = file.Length, - FileName = file.Name, + FileName = file.FileName, MimeType = file.ContentType, - IsImage = await assetThumbnailGenerator.IsValidImageAsync(fileContent) + IsImage = imageInfo != null, + PixelWidth = imageInfo?.PixelWidth, + PixelHeight = imageInfo?.PixelHeight }; fileContent.Position = 0; @@ -93,8 +150,8 @@ namespace Squidex.Controllers.Api.Assets var context = await CommandBus.PublishAsync(command); - var result = context.Result>().IdOrValue; - var response = new EntityCreatedDto { Id = result.ToString() }; + var result = context.Result>(); + var response = AssetDto.Create(command, result); return StatusCode(201, response); } diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs b/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs new file mode 100644 index 000000000..5f64319bd --- /dev/null +++ b/src/Squidex/Controllers/Api/Assets/Models/AssetDto.cs @@ -0,0 +1,107 @@ +// ========================================================================== +// AssetDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; +using NodaTime; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Write.Assets.Commands; + +namespace Squidex.Controllers.Api.Assets.Models +{ + public sealed class AssetDto + { + /// + /// The id of the asset. + /// + public Guid Id { get; set; } + + /// + /// The file name. + /// + [Required] + public string FileName { get; set; } + + /// + /// The mime type. + /// + [Required] + public string MimeType { get; set; } + + /// + /// The size of the file in bytes. + /// + public long FileSize { get; set; } + + /// + /// Determines of the created file is an image. + /// + public bool IsImage { get; set; } + + /// + /// The width of the image in pixels if the asset is an image. + /// + public int? PixelWidth { get; set; } + + /// + /// The height of the image in pixels if the asset is an image. + /// + public int? PixelHeight { get; set; } + + /// + /// The user that has created the schema. + /// + [Required] + public RefToken CreatedBy { get; set; } + + /// + /// The user that has updated the asset. + /// + [Required] + public RefToken LastModifiedBy { get; set; } + + /// + /// The date and time when the asset has been created. + /// + public Instant Created { get; set; } + + /// + /// The date and time when the asset has been modified last. + /// + public Instant LastModified { get; set; } + + /// + /// The version of the asset. + /// + public long Version { get; set; } + + public static AssetDto Create(CreateAsset command, EntityCreatedResult result) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var response = new AssetDto + { + Id = result.IdOrValue, + Version = result.Version, + Created = now, + CreatedBy = command.Actor, + LastModified = now, + LastModifiedBy = command.Actor, + FileName = command.FileName, + FileSize = command.FileSize, + MimeType = command.MimeType, + IsImage = command.IsImage, + PixelWidth = command.PixelWidth, + PixelHeight = command.PixelHeight + }; + + return response; + } + } +} diff --git a/src/Squidex/Controllers/Api/Assets/Models/AssetsDto.cs b/src/Squidex/Controllers/Api/Assets/Models/AssetsDto.cs new file mode 100644 index 000000000..0811b3f58 --- /dev/null +++ b/src/Squidex/Controllers/Api/Assets/Models/AssetsDto.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// AssetsDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Controllers.Api.Assets.Models +{ + public sealed class AssetsDto + { + /// + /// The total number of assets. + /// + public long Total { get; set; } + + /// + /// The assets. + /// + public AssetDto[] Items { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/EntityCreatedDto.cs b/src/Squidex/Controllers/Api/EntityCreatedDto.cs index b60d8dbb4..65297c183 100644 --- a/src/Squidex/Controllers/Api/EntityCreatedDto.cs +++ b/src/Squidex/Controllers/Api/EntityCreatedDto.cs @@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Controllers.Api { - public sealed class EntityCreatedDto + public class EntityCreatedDto { /// /// Id of the created entity. diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index cc002f906..1bfc5d291 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -64,7 +64,7 @@ namespace Squidex.Controllers.ContentApi await Task.WhenAll(taskForContents, taskForCount); - var model = new ContentsDto + var model = new AssetsDto { Total = taskForCount.Result, Items = taskForContents.Result.Take(200).Select(x => diff --git a/src/Squidex/Controllers/ContentApi/Models/ContentsDto.cs b/src/Squidex/Controllers/ContentApi/Models/ContentsDto.cs index fa8a42661..d1d594ca3 100644 --- a/src/Squidex/Controllers/ContentApi/Models/ContentsDto.cs +++ b/src/Squidex/Controllers/ContentApi/Models/ContentsDto.cs @@ -8,7 +8,7 @@ namespace Squidex.Controllers.ContentApi.Models { - public class ContentsDto + public class AssetsDto { /// /// The total number of content items. diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index 61caf9db5..dd70c9d5f 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -77,6 +77,7 @@ namespace Squidex var builder = new ContainerBuilder(); builder.Populate(services); + builder.RegisterModule(new AssetStoreModule(Configuration)); builder.RegisterModule(new EventPublishersModule(Configuration)); builder.RegisterModule(new EventStoreModule(Configuration)); builder.RegisterModule(new InfrastructureModule(Configuration)); diff --git a/src/Squidex/app/features/assets/declarations.ts b/src/Squidex/app/features/assets/declarations.ts index 0cc787227..260478be7 100644 --- a/src/Squidex/app/features/assets/declarations.ts +++ b/src/Squidex/app/features/assets/declarations.ts @@ -5,4 +5,5 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ +export * from './pages/asset.component'; export * from './pages/assets-page.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/assets/module.ts b/src/Squidex/app/features/assets/module.ts index a645c7739..41ee6a238 100644 --- a/src/Squidex/app/features/assets/module.ts +++ b/src/Squidex/app/features/assets/module.ts @@ -11,6 +11,7 @@ import { RouterModule, Routes } from '@angular/router'; import { SqxFrameworkModule } from 'shared'; import { + AssetComponent, AssetsPageComponent } from './declarations'; @@ -27,6 +28,7 @@ const routes: Routes = [ RouterModule.forChild(routes) ], declarations: [ + AssetComponent, AssetsPageComponent ] }) diff --git a/src/Squidex/app/features/assets/pages/asset.component.html b/src/Squidex/app/features/assets/pages/asset.component.html new file mode 100644 index 000000000..b54fbdcb8 --- /dev/null +++ b/src/Squidex/app/features/assets/pages/asset.component.html @@ -0,0 +1,7 @@ +
+
+
+ +
\ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/asset.component.scss b/src/Squidex/app/features/assets/pages/asset.component.scss new file mode 100644 index 000000000..da326d696 --- /dev/null +++ b/src/Squidex/app/features/assets/pages/asset.component.scss @@ -0,0 +1,11 @@ +@import '_vars'; +@import '_mixins'; + +$card-size: 16rem; + +.card { + width: $card-size; + height: $card-size; + margin-right: 1rem; + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/asset.component.ts b/src/Squidex/app/features/assets/pages/asset.component.ts new file mode 100644 index 000000000..7cd19189c --- /dev/null +++ b/src/Squidex/app/features/assets/pages/asset.component.ts @@ -0,0 +1,77 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, Input, OnInit } from '@angular/core'; + +import { + AppComponentBase, + AppsStoreService, + AssetDto, + AssetsService, + NotificationService, + UsersProviderService +} from 'shared'; + +@Component({ + selector: 'sqx-asset', + styleUrls: ['./asset.component.scss'], + templateUrl: './asset.component.html' +}) +export class AssetComponent extends AppComponentBase implements OnInit { + @Input() + public initFile: File; + + @Input() + public asset: AssetDto; + + public get fileInfo(): string { + let result = ''; + + if (this.asset != null) { + if (this.asset.pixelWidth) { + result = `${this.asset.pixelWidth}x${this.asset.pixelHeight}px, `; + } + + result += fileSize(this.asset.fileSize); + } + + return result; + } + + constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, + private readonly assetsService: AssetsService + ) { + super(notifications, users, apps); + } + + public ngOnInit() { + const initFile = this.initFile; + + if (initFile) { + this.appName() + .switchMap(app => this.assetsService.uploadFile(app, initFile)) + .subscribe(result => { + if (result instanceof AssetDto) { + this.asset = result; + } + }, error => { + this.notifyError(error); + }); + } + } +} + +function fileSize(b: number) { + let u = 0, s = 1024; + + while (b >= s || -b >= s) { + b /= s; + u++; + } + + return (u ? b.toFixed(1) + ' ' : b) + ' kMGTPEZY'[u] + 'B'; +} \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.html b/src/Squidex/app/features/assets/pages/assets-page.component.html index 0cf805c7e..5bb3cb4bf 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-page.component.html @@ -1,6 +1,6 @@ - +

Assets

@@ -13,7 +13,26 @@
+
+

Drop files here to upload

+ +
or
+ +
+ +
+ +
Drop file on existing item to replace the asset with a newer version.
+
+
+ + + + + + +
\ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.scss b/src/Squidex/app/features/assets/pages/assets-page.component.scss index fbb752506..ace5d94d5 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.scss +++ b/src/Squidex/app/features/assets/pages/assets-page.component.scss @@ -1,2 +1,24 @@ @import '_vars'; -@import '_mixins'; \ No newline at end of file +@import '_mixins'; + +.file-drop { + & { + border: 2px dashed $color-border; + background: transparent; + padding: 1rem; + text-align: center; + margin-bottom: 1rem; + } + + &-or { + font-size: .8rem; + } + + &-button { + margin: .5rem 0; + } + + &-info { + color: $color-subtext; + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.ts b/src/Squidex/app/features/assets/pages/assets-page.component.ts index dea529b8a..54f51f946 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.ts +++ b/src/Squidex/app/features/assets/pages/assets-page.component.ts @@ -5,15 +5,20 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; import { AppComponentBase, AppsStoreService, + AssetDto, + AssetsService, fadeAnimation, + ImmutableArray, NotificationService, + Pager, UsersProviderService - } from 'shared'; +} from 'shared'; @Component({ selector: 'sqx-assets-page', @@ -23,9 +28,58 @@ import { fadeAnimation ] }) -export class AssetsPageComponent extends AppComponentBase { - constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService) { +export class AssetsPageComponent extends AppComponentBase implements OnInit { + public newFiles = ImmutableArray.empty(); + + public assetsItems = ImmutableArray.empty(); + public assetsPager = new Pager(0); + public assetsFilter = new FormControl(); + public assertQuery = ''; + + constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, + private readonly assetsService: AssetsService + ) { super(notifications, users, apps); } + + public ngOnInit() { + this.load(); + } + + public search() { + this.assetsPager = new Pager(0); + this.assertQuery = this.assetsFilter.value; + + this.load(); + } + + private load() { + this.appName() + .switchMap(app => this.assetsService.getAssets(app, this.assetsPager.pageSize, this.assetsPager.skip, this.assertQuery, null)) + .subscribe(dtos => { + this.assetsItems = ImmutableArray.of(dtos.items); + this.assetsPager = this.assetsPager.setCount(dtos.total); + }, error => { + this.notifyError(error); + }); + } + + public goNext() { + this.assetsPager = this.assetsPager.goNext(); + + this.load(); + } + + public goPrev() { + this.assetsPager = this.assetsPager.goPrev(); + + this.load(); + } + + public onDrop(files: File[]) { + for (let file of files) { + this.newFiles = this.newFiles.pushFront(file); + } + } } diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts index 82fd16d64..8ba283d3b 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts @@ -15,7 +15,7 @@ import { fadeAnimation, NotificationService, UsersProviderService - } from 'shared'; +} from 'shared'; declare var _urq: any; diff --git a/src/Squidex/app/framework/angular/file-drop.directive.ts b/src/Squidex/app/framework/angular/file-drop.directive.ts new file mode 100644 index 000000000..7f24cb638 --- /dev/null +++ b/src/Squidex/app/framework/angular/file-drop.directive.ts @@ -0,0 +1,79 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Directive, EventEmitter, HostListener, Output } from '@angular/core'; + +@Directive({ + selector: '[sqxFileDrop]' +}) +export class FileDropDirective { + @Output('sqxFileDrop') + public drop = new EventEmitter(); + + @HostListener('dragenter', ['$event']) + public onDragEnter(event: DragDropEvent) { + this.tryStopEvent(event); + } + + @HostListener('dragover', ['$event']) + public onDragOver(event: DragDropEvent) { + this.tryStopEvent(event); + } + + @HostListener('drop', ['$event']) + public onDrop(event: DragDropEvent) { + const files: File[] = []; + + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < event.dataTransfer.files.length; i++) { + const file = event.dataTransfer.files[i]; + + files.push(file); + } + + this.drop.emit(files); + + this.stopEvent(event); + } + + private stopEvent(event: Event) { + event.preventDefault(); + event.stopPropagation(); + } + + private tryStopEvent(event: DragDropEvent) { + const hasFiles = this.hasFiles(event.dataTransfer.types); + + if (!hasFiles) { + return; + } + + this.stopEvent(event); + } + + private hasFiles(types: any): boolean { + if (!types) { + return false; + } + + if (isFunction(types.indexOf)) { + return types.indexOf('Files') !== -1; + } else if (isFunction(types.contains)) { + return types.contains('Files'); + } else { + return false; + } + } +} + +function isFunction(obj: any): boolean { + return !!(obj && obj.constructor && obj.call && obj.apply); +}; + +interface DragDropEvent extends MouseEvent { + readonly dataTransfer: DataTransfer; +} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 00d6e3df4..a506c6d58 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -13,6 +13,7 @@ export * from './angular/control-errors.component'; export * from './angular/copy.directive'; export * from './angular/date-time-editor.component'; export * from './angular/date-time.pipes'; +export * from './angular/file-drop.directive'; export * from './angular/focus-on-change.directive'; export * from './angular/focus-on-init.directive'; export * from './angular/geolocation-editor.component'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 50ef39dbd..22b4474b5 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -22,6 +22,7 @@ import { DayPipe, DisplayNamePipe, DurationPipe, + FileDropDirective, FocusOnChangeDirective, FocusOnInitDirective, FromNowPipe, @@ -72,6 +73,7 @@ import { DayPipe, DisplayNamePipe, DurationPipe, + FileDropDirective, FocusOnChangeDirective, FocusOnInitDirective, FromNowPipe, @@ -106,6 +108,7 @@ import { DayPipe, DisplayNamePipe, DurationPipe, + FileDropDirective, FocusOnChangeDirective, FocusOnInitDirective, FromNowPipe, diff --git a/src/Squidex/app/shared/services/assets.service.ts b/src/Squidex/app/shared/services/assets.service.ts index 38f8030b3..4a53f21ec 100644 --- a/src/Squidex/app/shared/services/assets.service.ts +++ b/src/Squidex/app/shared/services/assets.service.ts @@ -6,12 +6,43 @@ */ import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; +import { Headers, Http } from '@angular/http'; import { Observable } from 'rxjs'; -import { ApiUrlConfig, EntityCreatedDto } from 'framework'; +import { + ApiUrlConfig, + DateTime, + Version +} from 'framework'; + import { AuthService } from './auth.service'; +export class AssetsDto { + constructor( + public readonly total: number, + public readonly items: AssetDto[] + ) { + } +} + +export class AssetDto { + constructor( + public readonly id: string, + public readonly createdBy: string, + public readonly lastModifiedBy: string, + public readonly created: DateTime, + public readonly lastModified: DateTime, + public readonly fileName: string, + public readonly fileSize: number, + public readonly mimeType: string, + public readonly isImage: boolean, + public readonly pixelWidth: number | null, + public readonly pixelHeight: number | null, + public readonly version: Version + ) { + } +} + @Injectable() export class AssetsService { constructor( @@ -21,23 +52,89 @@ export class AssetsService { ) { } - public uploadFile(appName: string, file: File): Observable { - return new Observable(subscriber => { - const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); + public getAssets(appName: string, take: number, skip: number, query: string, mimeTypes: string[]): Observable { + let fullQuery = query ? query.trim() : ''; + + if (mimeTypes && mimeTypes.length > 0) { + let mimeQuery = '&mimeTypes='; + + for (let i = 0; i < mimeTypes.length; i++) { + mimeQuery += mimeTypes[0]; + + if (i > 0) { + mimeQuery += ','; + } + } + + fullQuery += mimeQuery; + } + + if (query && query.length > 0) { + fullQuery += `&query=${query}`; + } + + if (take > 0) { + fullQuery += `&take=${take}`; + } + + if (skip > 0) { + fullQuery += `&skip=${skip}`; + } + + const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/?${fullQuery}`); + + return this.authService.authGet(url) + .map(response => response.json()) + .map(response => { + const items: any[] = response.items; + + return new AssetsDto(response.total, items.map(item => { + return new AssetDto( + item.id, + item.createdBy, + item.lastModifiedBy, + DateTime.parseISO_UTC(item.created), + DateTime.parseISO_UTC(item.lastModified), + item.fileName, + item.fileSize, + item.mimeType, + item.isImage, + item.pixelWidth, + item.pixelHeight, + new Version(item.version.toString())); + })); + }) + .catchError('Failed to load assets. Please reload.'); + } + + public uploadFile(appName: string, file: File): Observable { + return new Observable(subscriber => { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/assets/`); const content = new FormData(); const headers = new Headers({ - 'Authorization': `${this.authService.user.user.token_type} ${this.authService.user.user.access_token}`, - 'Content-Type': 'multipart/form-data' + 'Authorization': `${this.authService.user.user.token_type} ${this.authService.user.user.access_token}` }); - content.append('file', file); + content.append('files', file); this.http - .post(url, content, headers) + .post(url, content, { headers }) .map(response => response.json()) .map(response => { - return new EntityCreatedDto(response.id); + return new AssetDto( + response.id, + response.createdBy, + response.lastModifiedBy, + DateTime.parseISO_UTC(response.created), + DateTime.parseISO_UTC(response.lastModified), + response.fileName, + response.fileSize, + response.mimeType, + response.isImage, + response.pixelWidth, + response.pixelHeight, + new Version(response.version.toString())); }) .catchError('Failed to upload asset. Please reload.') .subscribe(value => { diff --git a/tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs new file mode 100644 index 000000000..26367ea4a --- /dev/null +++ b/tests/Squidex.Write.Tests/Assets/AssetCommandHandlerTests.cs @@ -0,0 +1,93 @@ +// ========================================================================== +// AssetCommandHandlerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Write.Assets.Commands; +using Squidex.Write.TestHelpers; +using Xunit; + +// ReSharper disable ConvertToConstant.Local + +namespace Squidex.Write.Assets +{ + public class AssetCommandHandlerTests : HandlerTestBase + { + private readonly AssetCommandHandler sut; + private readonly AssetDomainObject asset; + private readonly Guid assetId = Guid.NewGuid(); + private readonly string fileName = "my-image.png"; + private readonly string mimeType = "image/png"; + private readonly long fileSize = 1024; + + public AssetCommandHandlerTests() + { + asset = new AssetDomainObject(assetId, 0); + + sut = new AssetCommandHandler(Handler); + } + + [Fact] + public async Task Create_should_create_asset() + { + var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, FileName = fileName, FileSize = fileSize, MimeType = mimeType }); + + await TestCreate(asset, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(assetId, context.Result>().IdOrValue); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + CreateAsset(); + + var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, FileSize = fileSize, MimeType = mimeType }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Rename_should_update_domain_object() + { + CreateAsset(); + + var context = CreateContextForCommand(new RenameAsset { AssetId = assetId, FileName = "my-new-image.png" }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateAsset(); + + var command = CreateContextForCommand(new DeleteAsset { AssetId = assetId }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void CreateAsset() + { + asset.Create(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }); + } + } +} diff --git a/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs new file mode 100644 index 000000000..e6ef96af5 --- /dev/null +++ b/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs @@ -0,0 +1,211 @@ +// ========================================================================== +// AssetDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Squidex.Write.Assets.Commands; +using Squidex.Write.TestHelpers; +using Xunit; + +// ReSharper disable ConvertToConstant.Local + +namespace Squidex.Write.Assets +{ + public class AssetDomainObjectTests : HandlerTestBase + { + private readonly AssetDomainObject sut; + private readonly string fileName = "my-image.png"; + private readonly string mimeType = "image/png"; + private readonly long fileSize = 1024; + + public Guid AssetId { get; } = Guid.NewGuid(); + + public AssetDomainObjectTests() + { + sut = new AssetDomainObject(AssetId, 0); + } + + [Fact] + public void Create_should_throw_if_created() + { + sut.Create(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType }); + + Assert.Throws(() => + { + sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); + }); + } + + [Fact] + public void Create_should_create_events() + { + sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetCreated { FileName = fileName, FileSize = fileSize, MimeType = mimeType }) + ); + } + + [Fact] + public void Update_should_throw_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType })); + }); + } + + [Fact] + public void Update_should_throw_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Update_should_create_events() + { + CreateAsset(); + + sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetUpdated { FileSize = fileSize, MimeType = mimeType }) + ); + } + + [Fact] + public void Rename_should_throw_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset { FileSize = fileSize, MimeType = mimeType })); + }); + } + + [Fact] + public void Rename_should_throw_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Rename_should_throw_if_command_is_not_valid() + { + CreateAsset(); + + Assert.Throws(() => + { + sut.Rename(CreateAssetCommand(new RenameAsset())); + }); + } + + [Fact] + public void Rename_should_throw_if_new_name_is_equal_to_old_name() + { + CreateAsset(); + + Assert.Throws(() => + { + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = fileName })); + }); + } + + [Fact] + public void Rename_should_create_events() + { + CreateAsset(); + + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "my-new-image.png" })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" }) + ); + } + + [Fact] + public void Delete_should_throw_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + }); + } + + [Fact] + public void Delete_should_throw_if_already_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + }); + } + + [Fact] + public void Delete_should_update_properties_create_events() + { + CreateAsset(); + + sut.Delete(CreateAssetCommand(new DeleteAsset())); + + Assert.True(sut.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetDeleted()) + ); + } + + private void CreateAsset() + { + sut.Create(CreateAssetCommand(new CreateAsset { FileName = fileName, FileSize = fileSize, MimeType = mimeType })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteAsset() + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + protected T CreateAssetEvent(T @event) where T : AssetEvent + { + @event.AssetId = AssetId; + + return CreateEvent(@event); + } + + protected T CreateAssetCommand(T command) where T : AssetAggregateCommand + { + command.AssetId = AssetId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs index ea2e8c1da..35f307a42 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs @@ -82,7 +82,7 @@ namespace Squidex.Write.Contents } [Fact] - public void Update_should_throw_if_schema_is_deleted() + public void Update_should_throw_if_content_is_deleted() { CreateContent(); DeleteContent(); @@ -108,7 +108,6 @@ namespace Squidex.Write.Contents public void Update_should_create_events() { CreateContent(); - UpdateContent(); sut.Update(CreateContentCommand(new UpdateContent { Data = otherData })); @@ -139,7 +138,7 @@ namespace Squidex.Write.Contents } [Fact] - public void Patch_should_throw_if_schema_is_deleted() + public void Patch_should_throw_if_content_is_deleted() { CreateContent(); DeleteContent(); @@ -165,7 +164,6 @@ namespace Squidex.Write.Contents public void Patch_should_create_events() { CreateContent(); - UpdateContent(); sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); @@ -196,7 +194,7 @@ namespace Squidex.Write.Contents } [Fact] - public void Publish_should_throw_if_schema_is_deleted() + public void Publish_should_throw_if_content_is_deleted() { CreateContent(); DeleteContent(); @@ -232,7 +230,7 @@ namespace Squidex.Write.Contents } [Fact] - public void Unpublish_should_throw_if_schema_is_deleted() + public void Unpublish_should_throw_if_content_is_deleted() { CreateContent(); DeleteContent();