From a1e0f3c979bfbb44909ab7d281cf3adf0dd27dde Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 7 Aug 2017 18:33:36 +0200 Subject: [PATCH] Source url for GraphQL. Closes #91 --- .../Contents/GraphQL/CachingGraphQLService.cs | 2 +- .../Contents/GraphQL/GraphQLModel.cs | 18 +++++++++++++++++- .../Contents/GraphQL/IGraphQLContext.cs | 4 ++++ .../Contents/GraphQL/IGraphQLUrlGenerator.cs | 4 ++++ .../Contents/GraphQL/Types/AssetGraphType.cs | 12 ++++++++++++ .../Assets/AzureBlobAssetStore.cs | 7 +++++++ .../Assets/GoogleCloudAssetStore.cs | 14 ++++++++++++-- .../Assets/FolderAssetStore.cs | 7 +++++++ .../Assets/IAssetStore.cs | 2 ++ src/Squidex/Config/Domain/ReadModule.cs | 14 +++++++++----- src/Squidex/Pipeline/GraphQLUrlGenerator.cs | 14 +++++++++++++- src/Squidex/appsettings.json | 6 +++++- .../Contents/GraphQLTests.cs | 4 ++++ .../Contents/TestData/FakeUrlGenerator.cs | 7 +++++++ .../Assets/AzureBlobAssetStoreTests.cs | 13 +++++++++++++ .../Assets/FolderAssetStoreTests.cs | 10 ++++++++++ .../Assets/GoogleCloudAssetStoreTests.cs | 13 +++++++++++++ 17 files changed, 140 insertions(+), 11 deletions(-) diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs index 41f626adb..5a2e32684 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL { var schemas = await schemaRepository.QueryAllAsync(app.Id); - modelContext = new GraphQLModel(app, schemas.Where(x => x.IsPublished)); + modelContext = new GraphQLModel(app, schemas.Where(x => x.IsPublished), urlGenerator); Cache.Set(cacheKey, modelContext); } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs index 8d27f3d1c..a3b9d57f0 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs @@ -41,10 +41,14 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL private readonly IGraphType assetListType; private readonly GraphQLSchema graphQLSchema; - public GraphQLModel(IAppEntity appEntity, IEnumerable schemas) + public bool CanGenerateAssetSourceUrl { get; } + + public GraphQLModel(IAppEntity appEntity, IEnumerable schemas, IGraphQLUrlGenerator urlGenerator) { this.appEntity = appEntity; + CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; + partitionResolver = appEntity.PartitionResolver; assetType = new AssetGraphType(this); @@ -113,6 +117,18 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL return resolver; } + public IFieldResolver ResolveAssetSourceUrl() + { + var resolver = new FuncFieldResolver(c => + { + var context = (QueryContext)c.UserContext; + + return context.UrlGenerator.GenerateAssetSourceUrl(appEntity, c.Source); + }); + + return resolver; + } + public IFieldResolver ResolveAssetThumbnailUrl() { var resolver = new FuncFieldResolver(c => diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs index 7b75f370a..4ad393783 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLContext.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL { public interface IGraphQLContext { + bool CanGenerateAssetSourceUrl { get; } + IFieldPartitioning ResolvePartition(Partitioning key); IGraphType GetAssetType(); @@ -25,6 +27,8 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL IFieldResolver ResolveAssetUrl(); + IFieldResolver ResolveAssetSourceUrl(); + IFieldResolver ResolveAssetThumbnailUrl(); IFieldResolver ResolveContentUrl(ISchemaEntity schemaEntity); diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs index c8e5ae4c5..3fa5fabb0 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLUrlGenerator.cs @@ -14,10 +14,14 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL { public interface IGraphQLUrlGenerator { + bool CanGenerateAssetSourceUrl { get; } + string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity); string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity); + string GenerateAssetSourceUrl(IAppEntity appEntity, IAssetEntity assetEntity); + string GenerateContentUrl(IAppEntity appEntity, ISchemaEntity schemaEntity, IContentEntity contentEntity); } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs index bec7228dd..b38bf49f5 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/AssetGraphType.cs @@ -148,6 +148,18 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types Description = "The height of the image in pixels if the asset is an image." }); + if (context.CanGenerateAssetSourceUrl) + { + + AddField(new FieldType + { + Name = "sourceUrl", + Resolver = context.ResolveAssetSourceUrl(), + ResolvedType = new StringGraphType(), + Description = "The source url of the asset." + }); + } + Description = "An asset"; } diff --git a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs index de86778a6..87a5d16a2 100644 --- a/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs +++ b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs @@ -50,6 +50,13 @@ namespace Squidex.Infrastructure.Assets } } + public string GenerateSourceUrl(string id, long version, string suffix) + { + var blobName = GetObjectName(id, version, suffix); + + return new Uri(blobContainer.StorageUri.PrimaryUri, $"/{containerName}/{blobName}").ToString(); + } + public async Task CopyTemporaryAsync(string name, string id, long version, string suffix) { var blobName = GetObjectName(id, version, suffix); diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs index da0edad51..6df5f83a8 100644 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs @@ -42,6 +42,13 @@ namespace Squidex.Infrastructure.Assets } } + public string GenerateSourceUrl(string id, long version, string suffix) + { + var objectName = GetObjectName(id, version, suffix); + + return $"https://storage.cloud.google.com/{bucketName}/{objectName}"; + } + public Task UploadTemporaryAsync(string name, Stream stream) { return storageClient.UploadObjectAsync(bucketName, name, "application/octet-stream", stream); @@ -88,9 +95,12 @@ namespace Squidex.Infrastructure.Assets { await storageClient.DeleteObjectAsync(bucketName, name); } - catch (GoogleApiException ex) when (ex.HttpStatusCode != HttpStatusCode.NotFound) + catch (GoogleApiException ex) { - throw; + if (ex.HttpStatusCode != HttpStatusCode.NotFound) + { + throw; + } } } diff --git a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs index b40b4e157..951503235 100644 --- a/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/FolderAssetStore.cs @@ -51,6 +51,13 @@ namespace Squidex.Infrastructure.Assets } } + public string GenerateSourceUrl(string id, long version, string suffix) + { + var file = GetFile(id, version, suffix); + + return file.FullName; + } + public async Task UploadTemporaryAsync(string name, Stream stream) { var file = GetFile(name); diff --git a/src/Squidex.Infrastructure/Assets/IAssetStore.cs b/src/Squidex.Infrastructure/Assets/IAssetStore.cs index 5612d273d..2505dedab 100644 --- a/src/Squidex.Infrastructure/Assets/IAssetStore.cs +++ b/src/Squidex.Infrastructure/Assets/IAssetStore.cs @@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.Assets { public interface IAssetStore { + string GenerateSourceUrl(string id, long version, string suffix); + Task CopyTemporaryAsync(string name, string id, long version, string suffix); Task DownloadAsync(string id, long version, string suffix, Stream stream); diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index 85fee06ea..b91030ef2 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -23,6 +23,7 @@ using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Read.Schemas.Services.Implementations; using Squidex.Domain.Users; using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.CQRS.Events; using Squidex.Pipeline; @@ -46,6 +47,14 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); + builder.Register(c => new GraphQLUrlGenerator( + c.Resolve>(), + c.Resolve(), + Configuration.GetValue("assetStore:exposeSourceUrl"))) + .As() + .AsSelf() + .SingleInstance(); + builder.RegisterType() .As() .AsSelf() @@ -61,11 +70,6 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - builder.RegisterType() .As() .AsSelf() diff --git a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs index de5a38495..437aa691b 100644 --- a/src/Squidex/Pipeline/GraphQLUrlGenerator.cs +++ b/src/Squidex/Pipeline/GraphQLUrlGenerator.cs @@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Read.Assets; using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Infrastructure.Assets; // ReSharper disable ConvertIfStatementToReturnStatement @@ -20,11 +21,17 @@ namespace Squidex.Pipeline { public sealed class GraphQLUrlGenerator : IGraphQLUrlGenerator { + private readonly IAssetStore assetStore; private readonly MyUrlsOptions urlsOptions; - public GraphQLUrlGenerator(IOptions urlsOptions) + public bool CanGenerateAssetSourceUrl { get; } + + public GraphQLUrlGenerator(IOptions urlsOptions, IAssetStore assetStore, bool allowAssetSourceUrl) { + this.assetStore = assetStore; this.urlsOptions = urlsOptions.Value; + + CanGenerateAssetSourceUrl = allowAssetSourceUrl; } public string GenerateAssetThumbnailUrl(IAppEntity appEntity, IAssetEntity assetEntity) @@ -46,5 +53,10 @@ namespace Squidex.Pipeline { return urlsOptions.BuildUrl($"api/content/{appEntity.Name}/{schemaEntity.Name}/{contentEntity.Id}"); } + + public string GenerateAssetSourceUrl(IAppEntity appEntity, IAssetEntity assetEntity) + { + return assetStore.GenerateSourceUrl(assetEntity.Id.ToString(), assetEntity.FileVersion, null); + } } } diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 08409956d..184e460e9 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -61,7 +61,11 @@ * The connection string to the azure storage service. */ "connectionString": "UseDevelopmentStorage=true" - } + }, + /* + * Allow to expose the url in graph ql url. + */ + "exposeSourceUrl": false }, "eventStore": { diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs index 0766b607a..56dd1acf4 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs @@ -98,6 +98,7 @@ namespace Squidex.Domain.Apps.Read.Contents lastModifiedBy url thumbnailUrl + sourceUrl mimeType fileName fileSize @@ -133,6 +134,7 @@ namespace Squidex.Domain.Apps.Read.Contents lastModifiedBy = "subject:user2", url = $"assets/{assetEntity.Id}", thumbnailUrl = $"assets/{assetEntity.Id}?width=100", + sourceUrl = $"assets/source/{assetEntity.Id}", mimeType = "image/png", fileName = "MyFile.png", fileSize = 1024, @@ -165,6 +167,7 @@ namespace Squidex.Domain.Apps.Read.Contents lastModifiedBy url thumbnailUrl + sourceUrl mimeType fileName fileSize @@ -194,6 +197,7 @@ namespace Squidex.Domain.Apps.Read.Contents lastModifiedBy = "subject:user2", url = $"assets/{assetEntity.Id}", thumbnailUrl = $"assets/{assetEntity.Id}?width=100", + sourceUrl = $"assets/source/{assetEntity.Id}", mimeType = "image/png", fileName = "MyFile.png", fileSize = 1024, diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs index 1b800f71e..5bbfb3a24 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeUrlGenerator.cs @@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Read.Contents.TestData { public sealed class FakeUrlGenerator : IGraphQLUrlGenerator { + public bool CanGenerateAssetSourceUrl { get; } = true; + public string GenerateAssetUrl(IAppEntity appEntity, IAssetEntity assetEntity) { return $"assets/{assetEntity.Id}"; @@ -25,6 +27,11 @@ namespace Squidex.Domain.Apps.Read.Contents.TestData return $"assets/{assetEntity.Id}?width=100"; } + public string GenerateAssetSourceUrl(IAppEntity appEntity, IAssetEntity assetEntity) + { + return $"assets/source/{assetEntity.Id}"; + } + public string GenerateContentUrl(IAppEntity appEntity, ISchemaEntity schemaEntity, IContentEntity contentEntity) { return $"contents/{schemaEntity.Name}/{contentEntity.Id}"; diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs index a959b6614..eabd6bb77 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs @@ -6,6 +6,9 @@ // All rights reserved. // ========================================================================== +using System; +using Xunit; + namespace Squidex.Infrastructure.Assets { internal class AzureBlobAssetStoreTests : AssetStoreTests @@ -18,5 +21,15 @@ namespace Squidex.Infrastructure.Assets public override void Dispose() { } + + [Fact] + public void Should_calculate_source_url() + { + Sut.Connect(); + + var id = Guid.NewGuid().ToString(); + + Assert.Equal($"http://127.0.0.1:10000/squidex-test-container/{id}_1", Sut.GenerateSourceUrl(id, 1, null)); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs index c442b4220..721b8465f 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/FolderAssetStoreTests.cs @@ -45,6 +45,16 @@ namespace Squidex.Infrastructure.Assets Assert.True(Directory.Exists(testFolder)); } + [Fact] + public void Should_calculate_source_url() + { + Sut.Connect(); + + var id = Guid.NewGuid().ToString(); + + Assert.Equal(Path.Combine(testFolder, $"{id}_1"), Sut.GenerateSourceUrl(id, 1, null)); + } + private static string CreateInvalidPath() { var windir = Environment.GetEnvironmentVariable("windir"); diff --git a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs index 085c28a06..e38da3b4d 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/GoogleCloudAssetStoreTests.cs @@ -6,6 +6,9 @@ // All rights reserved. // ========================================================================== +using System; +using Xunit; + namespace Squidex.Infrastructure.Assets { internal class GoogleCloudAssetStoreTests : AssetStoreTests @@ -18,5 +21,15 @@ namespace Squidex.Infrastructure.Assets public override void Dispose() { } + + [Fact] + public void Should_calculate_source_url() + { + Sut.Connect(); + + var id = Guid.NewGuid().ToString(); + + Assert.Equal($"https://storage.cloud.google.com/squidex-test/{id}_1", Sut.GenerateSourceUrl(id, 1, null)); + } } }