diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs index 1f373f154..cd7cfda36 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -63,6 +63,37 @@ namespace Squidex.Domain.Apps.Core.ConvertContent }; } + public static FieldConverter ResolveAssetUrls(ISet fields, IAssetUrlGenerator urlGenerator) + { + if (fields?.Any() != true) + { + return (data, field) => data; + } + + var isAll = fields.First() == "*"; + + return (data, field) => + { + if (field is IField assetField && (isAll || fields.Contains(field.Name))) + { + foreach (var partition in data) + { + if (partition.Value is JArray array) + { + for (var i = 0; i < array.Count; i++) + { + var id = array[i].ToString(); + + array[i] = urlGenerator.GenerateUrl(id); + } + } + } + } + + return data; + }; + } + public static FieldConverter ResolveInvariant(LanguagesConfig config) { var codeForInvariant = InvariantPartitioning.Instance.Master.Key; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs new file mode 100644 index 000000000..1a681e9c9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public interface IAssetUrlGenerator + { + string GenerateUrl(string assetId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryContext.cs deleted file mode 100644 index 547cab20e..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryContext.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentQueryContext : Cloneable - { - public string SchemaIdOrName { get; private set; } - - public QueryContext Base { get; private set; } - - public ContentQueryContext(QueryContext @base) - { - Guard.NotNull(@base, nameof(@base)); - - Base = @base; - } - - public ContentQueryContext WithSchemaName(string name) - { - return Clone(c => c.SchemaIdOrName = name); - } - - public ContentQueryContext WithArchived(bool archived) - { - return Clone(c => c.Base = c.Base.WithArchived(archived)); - } - - public ContentQueryContext WithFlatten(bool flatten) - { - return Clone(c => c.Base = c.Base.WithFlatten(flatten)); - } - - public ContentQueryContext WithUnpublished(bool unpublished) - { - return Clone(c => c.Base = c.Base.WithUnpublished(unpublished)); - } - - public ContentQueryContext WithSchemaId(Guid id) - { - return Clone(c => c.SchemaIdOrName = id.ToString()); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index 6160796fa..d0a326f76 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -40,12 +40,14 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly IContentRepository contentRepository; private readonly IContentVersionLoader contentVersionLoader; private readonly IAppProvider appProvider; + private readonly IAssetUrlGenerator assetUrlGenerator; private readonly IScriptEngine scriptEngine; private readonly ContentOptions options; private readonly EdmModelBuilder modelBuilder; public ContentQueryService( IAppProvider appProvider, + IAssetUrlGenerator assetUrlGenerator, IContentRepository contentRepository, IContentVersionLoader contentVersionLoader, IScriptEngine scriptEngine, @@ -53,6 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Contents EdmModelBuilder modelBuilder) { Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader)); Guard.NotNull(modelBuilder, nameof(modelBuilder)); @@ -60,6 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Guard.NotNull(scriptEngine, nameof(scriptEngine)); this.appProvider = appProvider; + this.assetUrlGenerator = assetUrlGenerator; this.contentRepository = contentRepository; this.contentVersionLoader = contentVersionLoader; this.modelBuilder = modelBuilder; @@ -67,66 +71,66 @@ namespace Squidex.Domain.Apps.Entities.Contents this.scriptEngine = scriptEngine; } - public Task ThrowIfSchemaNotExistsAsync(ContentQueryContext context) + public Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName) { - return GetSchemaAsync(context); + return GetSchemaAsync(context, schemaIdOrName); } - public async Task FindContentAsync(ContentQueryContext context, Guid id, long version = -1) + public async Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1) { Guard.NotNull(context, nameof(context)); - var schema = await GetSchemaAsync(context); + var schema = await GetSchemaAsync(context, schemaIdOrName); - CheckPermission(schema, context.Base.User); + CheckPermission(schema, context.User); using (Profiler.TraceMethod()) { var isVersioned = version > EtagVersion.Empty; - var status = GetFindStatus(context.Base); + var status = GetFindStatus(context); var content = isVersioned ? await FindContentByVersionAsync(id, version) : - await FindContentAsync(context.Base, id, status, schema); + await FindContentAsync(context, id, status, schema); - if (content == null || (content.Status != Status.Published && !context.Base.IsFrontendClient) || content.SchemaId.Id != schema.Id) + if (content == null || (content.Status != Status.Published && !context.IsFrontendClient) || content.SchemaId.Id != schema.Id) { throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); } - return Transform(context.Base, schema, true, content); + return Transform(context, schema, true, content); } } - public async Task> QueryAsync(ContentQueryContext context, Q query) + public async Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query) { Guard.NotNull(context, nameof(context)); - var schema = await GetSchemaAsync(context); + var schema = await GetSchemaAsync(context, schemaIdOrName); - CheckPermission(schema, context.Base.User); + CheckPermission(schema, context.User); using (Profiler.TraceMethod()) { - var status = GetQueryStatus(context.Base); + var status = GetQueryStatus(context); IResultList contents; if (query.Ids?.Count > 0) { - contents = await contentRepository.QueryAsync(context.Base.App, schema, status, new HashSet(query.Ids)); + contents = await contentRepository.QueryAsync(context.App, schema, status, new HashSet(query.Ids)); contents = Sort(contents, query.Ids); } else { - var parsedQuery = ParseQuery(context.Base, query.ODataQuery, schema); + var parsedQuery = ParseQuery(context, query.ODataQuery, schema); - contents = await contentRepository.QueryAsync(context.Base.App, schema, status, parsedQuery); + contents = await contentRepository.QueryAsync(context.App, schema, status, parsedQuery); } - return Transform(context.Base, schema, true, contents); + return Transform(context, schema, true, contents); } } @@ -183,7 +187,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private static IEnumerable GenerateConverters(QueryContext context, bool checkType) + private IEnumerable GenerateConverters(QueryContext context, bool checkType) { if (!context.IsFrontendClient) { @@ -208,6 +212,11 @@ namespace Squidex.Domain.Apps.Entities.Contents { yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, context.Languages); } + + if (context.AssetFieldsToResolve?.Any() == true) + { + yield return FieldConverters.ResolveAssetUrls(context.AssetFieldsToResolve, assetUrlGenerator); + } } } @@ -244,23 +253,23 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public async Task GetSchemaAsync(ContentQueryContext context) + public async Task GetSchemaAsync(QueryContext context, string schemaIdOrName) { ISchemaEntity schema = null; - if (Guid.TryParse(context.SchemaIdOrName, out var id)) + if (Guid.TryParse(schemaIdOrName, out var id)) { - schema = await appProvider.GetSchemaAsync(context.Base.App.Id, id); + schema = await appProvider.GetSchemaAsync(context.App.Id, id); } if (schema == null) { - schema = await appProvider.GetSchemaAsync(context.Base.App.Id, context.SchemaIdOrName); + schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName); } if (schema == null) { - throw new DomainObjectNotFoundException(context.SchemaIdOrName, typeof(ISchemaEntity)); + throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); } return schema; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index c2a8c53b1..0512c3c66 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -13,10 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentQueryService { - Task> QueryAsync(ContentQueryContext context, Q query); + Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query); - Task FindContentAsync(ContentQueryContext context, Guid id, long version = EtagVersion.Any); + Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any); - Task ThrowIfSchemaNotExistsAsync(ContentQueryContext context); + Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs index 43f7d52e4..666b7474d 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (content == null) { - content = await contentQuery.FindContentAsync(new ContentQueryContext(context).WithSchemaId(schemaId), id); + content = await contentQuery.FindContentAsync(context, schemaId.ToString(), id); if (content != null) { @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public async Task> QueryContentsAsync(string schemaIdOrName, string query) { - var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaName(schemaIdOrName), Q.Empty.WithODataQuery(query)); + var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); foreach (var content in result) { @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (notLoadedContents.Count > 0) { - var result = await contentQuery.QueryAsync(new ContentQueryContext(context).WithSchemaId(schemaId), Q.Empty.WithIds(notLoadedContents)); + var result = await contentQuery.QueryAsync(context, schemaId.ToString(), Q.Empty.WithIds(notLoadedContents)); foreach (var content in result) { diff --git a/src/Squidex.Domain.Apps.Entities/QueryContext.cs b/src/Squidex.Domain.Apps.Entities/QueryContext.cs index c8cd33d2a..6ee665105 100644 --- a/src/Squidex.Domain.Apps.Entities/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Entities/QueryContext.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Security.Claims; using Squidex.Domain.Apps.Entities.Apps; @@ -15,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities { public sealed class QueryContext : Cloneable { + private static readonly char[] Separators = { ',', ';' }; + public ClaimsPrincipal User { get; private set; } public IAppEntity App { get; private set; } @@ -25,7 +28,9 @@ namespace Squidex.Domain.Apps.Entities public bool Unpublished { get; set; } - public IEnumerable Languages { get; private set; } + public ISet AssetFieldsToResolve { get; private set; } + + public ISet Languages { get; private set; } private QueryContext() { @@ -51,19 +56,45 @@ namespace Squidex.Domain.Apps.Entities return Clone(c => c.Flatten = flatten); } + public QueryContext WithAssetFieldsToResolve(IEnumerable fieldNames) + { + if (fieldNames != null) + { + return Clone(c => + { + var fields = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var part in fieldNames) + { + foreach (var fieldName in part.Split(Separators, StringSplitOptions.RemoveEmptyEntries)) + { + fields.Add(fieldName.Trim()); + } + } + + c.AssetFieldsToResolve = fields; + }); + } + + return this; + } + public QueryContext WithLanguages(IEnumerable languageCodes) { if (languageCodes != null) { return Clone(c => { - var languages = new List(); + var languages = new HashSet(); - foreach (var iso2Code in languageCodes) + foreach (var part in languageCodes) { - if (Language.TryGetLanguage(iso2Code, out var language)) + foreach (var iso2Code in part.Split(Separators, StringSplitOptions.RemoveEmptyEntries)) { - languages.Add(language); + if (Language.TryGetLanguage(iso2Code.Trim(), out var language)) + { + languages.Add(language); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 8644d3fe0..d85bd3583 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -61,7 +61,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(2)] public async Task PostGraphQL(string app, [FromBody] GraphQLQuery query) { - var result = await graphQl.QueryAsync(Context().Base, query); + var result = await graphQl.QueryAsync(Context(), query); if (result.HasError) { @@ -92,7 +92,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(2)] public async Task PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch) { - var result = await graphQl.QueryAsync(Context().Base, batch); + var result = await graphQl.QueryAsync(Context(), batch); if (result.HasError) { @@ -124,14 +124,14 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(2)] public async Task GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null) { - var context = Context().WithArchived(archived).WithSchemaName(name); + var context = Context().WithArchived(archived); - var result = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); + var result = await contentQuery.QueryAsync(context, name, Q.Empty.WithIds(ids).WithODataQuery(Request.QueryString.ToString())); var response = new ContentsDto { Total = result.Total, - Items = result.Take(200).Select(x => ContentDto.FromContent(x, context.Base)).ToArray() + Items = result.Take(200).Select(x => ContentDto.FromContent(x, context)).ToArray() }; if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) @@ -163,10 +163,10 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task GetContent(string app, string name, Guid id) { - var context = Context().WithSchemaName(name); - var content = await contentQuery.FindContentAsync(context, id); + var context = Context(); + var content = await contentQuery.FindContentAsync(context, name, id); - var response = ContentDto.FromContent(content, context.Base); + var response = ContentDto.FromContent(content, context); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -199,10 +199,10 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task GetContentVersion(string app, string name, Guid id, int version) { - var context = Context().WithSchemaName(name); - var content = await contentQuery.FindContentAsync(context, id, version); + var context = Context(); + var content = await contentQuery.FindContentAsync(context, name, id, version); - var response = ContentDto.FromContent(content, context.Base); + var response = ContentDto.FromContent(content, context); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -235,7 +235,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish }; @@ -269,7 +269,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new UpdateContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; var context = await CommandBus.PublishAsync(command); @@ -302,7 +302,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new PatchContent { ContentId = id, Data = request.ToCleaned(), AsDraft = asDraft }; var context = await CommandBus.PublishAsync(command); @@ -334,7 +334,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task PublishContent(string app, string name, Guid id, string dueTime = null) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Published, dueTime); @@ -364,7 +364,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task UnpublishContent(string app, string name, Guid id, string dueTime = null) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Draft, dueTime); @@ -394,7 +394,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task ArchiveContent(string app, string name, Guid id, string dueTime = null) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Archived, dueTime); @@ -424,7 +424,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task RestoreContent(string app, string name, Guid id, string dueTime = null) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = CreateCommand(id, Status.Draft, dueTime); @@ -453,7 +453,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task DiscardChanges(string app, string name, Guid id) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new DiscardChanges { ContentId = id }; @@ -481,7 +481,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(1)] public async Task DeleteContent(string app, string name, Guid id) { - await contentQuery.ThrowIfSchemaNotExistsAsync(Context().WithSchemaName(name)); + await contentQuery.ThrowIfSchemaNotExistsAsync(Context(), name); var command = new DeleteContent { ContentId = id }; @@ -507,11 +507,12 @@ namespace Squidex.Areas.Api.Controllers.Contents return new ChangeContentStatus { Status = status, ContentId = id, DueTime = dt }; } - private ContentQueryContext Context() + private QueryContext Context() { - return new ContentQueryContext(QueryContext.Create(App, User) - .WithLanguages(Request.Headers["X-Languages"])) + return QueryContext.Create(App, User) + .WithAssetFieldsToResolve(Request.Headers["X-AssetFields"]) .WithFlatten(Request.Headers.ContainsKey("X-Flatten")) + .WithLanguages(Request.Headers["X-Languages"]) .WithUnpublished(Request.Headers.ContainsKey("X-Unpublished")); } } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index 12cc6608c..413938e1c 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -13,6 +13,7 @@ using Migrate_01; using Migrate_01.Migrations; using Orleans; using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Tags; @@ -60,7 +61,7 @@ namespace Squidex.Config.Domain c.GetRequiredService>(), c.GetRequiredService(), exposeSourceUrl)) - .As().As(); + .As().As().As(); services.AddSingletonAs() .As() diff --git a/src/Squidex/Pipeline/UrlGenerator.cs b/src/Squidex/Pipeline/UrlGenerator.cs index 48249b15b..cf73bdd3d 100644 --- a/src/Squidex/Pipeline/UrlGenerator.cs +++ b/src/Squidex/Pipeline/UrlGenerator.cs @@ -8,6 +8,7 @@ using System; using Microsoft.Extensions.Options; using Squidex.Config; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; @@ -19,7 +20,7 @@ using Squidex.Infrastructure.Assets; namespace Squidex.Pipeline { - public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator + public sealed class UrlGenerator : IGraphQLUrlGenerator, IRuleUrlGenerator, IAssetUrlGenerator { private readonly IAssetStore assetStore; private readonly MyUrlsOptions urlsOptions; @@ -44,6 +45,11 @@ namespace Squidex.Pipeline return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}&width=100&mode=Max"); } + public string GenerateUrl(string assetId) + { + return urlsOptions.BuildUrl($"api/assets/{assetId}"); + } + public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) { return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.Version}"); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs index 9f07fbe82..58417c0c9 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs @@ -5,7 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Linq; +using FakeItEasy; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; @@ -18,15 +20,23 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent { public class FieldConvertersTests { + private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake(); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); - private readonly RootField numberField = Fields.Number(1, "1", Partitioning.Invariant); + private readonly RootField jsonField = Fields.Json(1, "1", Partitioning.Invariant); private readonly RootField stringLanguageField = Fields.String(1, "1", Partitioning.Language); private readonly RootField stringInvariantField = Fields.String(1, "1", Partitioning.Invariant); - private readonly RootField jsonField = Fields.Json(1, "1", Partitioning.Invariant); + private readonly RootField numberField = Fields.Number(1, "1", Partitioning.Invariant); + private readonly RootField assetsField = Fields.Assets(1, "1", Partitioning.Invariant); private readonly RootField arrayField = Fields.Array(1, "1", Partitioning.Invariant, Fields.Number(1, "field1"), Fields.Number(2, "field2").Hide()); + public FieldConvertersTests() + { + A.CallTo(() => assetUrlGenerator.GenerateUrl(A.Ignored)) + .ReturnsLazily(ctx => $"url/to/{ctx.GetArgument(0)}"); + } + [Fact] public void Should_filter_for_value_conversion() { @@ -406,5 +416,69 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent Assert.Same(source, result); } + + [Fact] + public void Should_convert_asset_ids_to_urls() + { + var source = + new ContentFieldData() + .AddValue("iv", new JArray("1", "2")); + + var expected = + new ContentFieldData() + .AddValue("iv", new JArray("url/to/1", "url/to/2")); + + var rtesult = FieldConverters.ResolveAssetUrls(new HashSet(new[] { "1" }), assetUrlGenerator)(source, assetsField); + + Assert.Equal(expected, source); + } + + [Fact] + public void Should_convert_asset_ids_to_urls_for_wildcard_fields() + { + var source = + new ContentFieldData() + .AddValue("iv", new JArray("1", "2")); + + var expected = + new ContentFieldData() + .AddValue("iv", new JArray("url/to/1", "url/to/2")); + + var rtesult = FieldConverters.ResolveAssetUrls(new HashSet(new[] { "*" }), assetUrlGenerator)(source, assetsField); + + Assert.Equal(expected, source); + } + + [Fact] + public void Should_not_convert_asset_ids_to_urls_when_field_does_not_match() + { + var source = + new ContentFieldData() + .AddValue("iv", new JArray("1", "2")); + + var expected = + new ContentFieldData() + .AddValue("iv", new JArray("1", "2")); + + var rtesult = FieldConverters.ResolveAssetUrls(new HashSet(new[] { "2" }), assetUrlGenerator)(source, assetsField); + + Assert.Equal(expected, source); + } + + [Fact] + public void Should_not_convert_asset_ids_to_urls_when_field_is_specified() + { + var source = + new ContentFieldData() + .AddValue("iv", new JArray("1", "2")); + + var expected = + new ContentFieldData() + .AddValue("iv", new JArray("1", "2")); + + var rtesult = FieldConverters.ResolveAssetUrls(null, assetUrlGenerator)(source, assetsField); + + Assert.Equal(expected, source); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 6318c1272..e23488ba2 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Options; using Microsoft.OData; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Apps; @@ -40,6 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly ISchemaEntity schema = A.Fake(); private readonly IAppEntity app = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); + private readonly IAssetUrlGenerator urlGenerator = A.Fake(); private readonly Guid appId = Guid.NewGuid(); private readonly Guid schemaId = Guid.NewGuid(); private readonly Guid contentId = Guid.NewGuid(); @@ -51,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly ClaimsPrincipal user; private readonly ClaimsIdentity identity = new ClaimsIdentity(); private readonly EdmModelBuilder modelBuilder = A.Fake(); - private readonly ContentQueryContext context; + private readonly QueryContext context; private readonly ContentQueryService sut; public ContentQueryServiceTests() @@ -68,9 +70,15 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaName)); A.CallTo(() => schema.ScriptQuery).Returns(script); - context = new ContentQueryContext(QueryContext.Create(app, user)); + context = QueryContext.Create(app, user); - sut = new ContentQueryService(appProvider, contentRepository, contentVersionLoader, scriptEngine, Options.Create(new ContentOptions()), modelBuilder); + sut = new ContentQueryService( + appProvider, + urlGenerator, + contentRepository, + contentVersionLoader, + scriptEngine, + Options.Create(new ContentOptions()), modelBuilder); } [Fact] @@ -78,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchema(); - var result = await sut.GetSchemaAsync(context.WithSchemaId(schemaId)); + var result = await sut.GetSchemaAsync(context, schemaId.ToString()); Assert.Equal(schema, result); } @@ -88,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchema(); - var result = await sut.GetSchemaAsync(context.WithSchemaName(schemaName)); + var result = await sut.GetSchemaAsync(context, schemaName); Assert.Equal(schema, result); } @@ -98,7 +106,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchemaNotFound(); - await Assert.ThrowsAsync(() => sut.GetSchemaAsync(context.WithSchemaName(schemaName))); + var ctx = context; + + await Assert.ThrowsAsync(() => sut.GetSchemaAsync(ctx, schemaName)); } [Fact] @@ -106,7 +116,9 @@ namespace Squidex.Domain.Apps.Entities.Contents { SetupSchemaNotFound(); - await Assert.ThrowsAsync(() => sut.ThrowIfSchemaNotExistsAsync(context.WithSchemaName(schemaName))); + var ctx = context; + + await Assert.ThrowsAsync(() => sut.ThrowIfSchemaNotExistsAsync(ctx, schemaName)); } public static IEnumerable SingleDataFrontend = new[] @@ -127,9 +139,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupClaims(false, false); SetupSchema(); - var ctx = context.WithSchemaId(schemaId); + var ctx = context; - await Assert.ThrowsAsync(() => sut.FindContentAsync(ctx, contentId)); + await Assert.ThrowsAsync(() => sut.FindContentAsync(ctx, schemaId.ToString(), contentId)); } [Fact] @@ -141,9 +153,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentRepository.FindContentAsync(app, schema, new[] { Status.Published }, contentId)) .Returns((IContentEntity)null); - var ctx = context.WithSchemaId(schemaId); + var ctx = context; - await Assert.ThrowsAsync(async () => await sut.FindContentAsync(ctx, contentId)); + await Assert.ThrowsAsync(async () => await sut.FindContentAsync(ctx, schemaId.ToString(), contentId)); } [Theory] @@ -159,9 +171,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.IsSameSequenceAs(status), contentId)) .Returns(content); - var ctx = context.WithSchemaId(schemaId).WithUnpublished(unpublished); + var ctx = context.WithUnpublished(unpublished); - var result = await sut.FindContentAsync(ctx, contentId); + var result = await sut.FindContentAsync(ctx, schemaId.ToString(), contentId); Assert.Equal(contentTransformed, result.Data); Assert.Equal(content.Id, result.Id); @@ -183,9 +195,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentRepository.FindContentAsync(app, schema, A.That.IsSameSequenceAs(status), contentId)) .Returns(content); - var ctx = context.WithSchemaId(schemaId).WithUnpublished(unpublished); + var ctx = context.WithUnpublished(unpublished); - var result = await sut.FindContentAsync(ctx, contentId); + var result = await sut.FindContentAsync(ctx, schemaId.ToString(), contentId); Assert.Equal(contentTransformed, result.Data); Assert.Equal(content.Id, result.Id); @@ -206,9 +218,9 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentVersionLoader.LoadAsync(contentId, 10)) .Returns(content); - var ctx = context.WithSchemaId(schemaId); + var ctx = context; - var result = await sut.FindContentAsync(ctx, contentId, 10); + var result = await sut.FindContentAsync(ctx, schemaId.ToString(), contentId, 10); Assert.Equal(contentTransformed, result.Data); Assert.Equal(content.Id, result.Id); @@ -236,9 +248,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupClaims(false, false); SetupSchema(); - var ctx = context.WithSchemaId(schemaId); + var ctx = context; - await Assert.ThrowsAsync(() => sut.QueryAsync(ctx, Q.Empty)); + await Assert.ThrowsAsync(() => sut.QueryAsync(ctx, schemaId.ToString(), Q.Empty)); } [Theory] @@ -254,9 +266,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupScripting(contentId); SetupContents(status, count, total, content); - var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); - var result = await sut.QueryAsync(ctx, Q.Empty); + var result = await sut.QueryAsync(ctx, schemaId.ToString(), Q.Empty); Assert.Equal(contentData, result[0].Data); Assert.Equal(content.Id, result[0].Id); @@ -280,9 +292,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupScripting(contentId); SetupContents(status, count, total, content); - var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); - var result = await sut.QueryAsync(ctx, Q.Empty); + var result = await sut.QueryAsync(ctx, schemaId.ToString(), Q.Empty); Assert.Equal(contentData, result[0].Data); Assert.Equal(contentId, result[0].Id); @@ -302,7 +314,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => modelBuilder.BuildEdmModel(schema, app)) .Throws(new ODataException()); - return Assert.ThrowsAsync(() => sut.QueryAsync(context.WithSchemaId(schemaId), Q.Empty.WithODataQuery("query"))); + return Assert.ThrowsAsync(() => sut.QueryAsync(context, schemaId.ToString(), Q.Empty.WithODataQuery("query"))); } public static IEnumerable ManyIdDataFrontend = new[] @@ -334,9 +346,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupScripting(ids.ToArray()); SetupContents(status, total, ids); - var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); - var result = await sut.QueryAsync(ctx, Q.Empty.WithIds(ids)); + var result = await sut.QueryAsync(ctx, schemaId.ToString(), Q.Empty.WithIds(ids)); Assert.Equal(ids, result.Select(x => x.Id).ToList()); Assert.Equal(total, result.Total); @@ -358,9 +370,9 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupScripting(ids.ToArray()); SetupContents(status, total, ids); - var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished); + var ctx = context.WithArchived(archive).WithUnpublished(unpublished); - var result = await sut.QueryAsync(ctx, Q.Empty.WithIds(ids)); + var result = await sut.QueryAsync(ctx, schemaId.ToString(), Q.Empty.WithIds(ids)); Assert.Equal(ids, result.Select(x => x.Id).ToList()); Assert.Equal(total, result.Total); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index c9c197dab..f2a90e843 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -283,7 +283,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) .Returns(ResultList.Create(0, content)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -422,7 +422,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5"))) .Returns(ResultList.Create(10, content)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -547,7 +547,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) .Returns(content); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -642,10 +642,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) .Returns(content); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.Ignored)) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.ToString(), A.Ignored)) .Returns(ResultList.Create(0, contentRef)); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -700,7 +700,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) .Returns(content); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) @@ -814,7 +814,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), contentId, EtagVersion.Any)) + A.CallTo(() => contentQuery.FindContentAsync(MatchsContentContext(), schemaId.ToString(), contentId, EtagVersion.Any)) .Returns(content); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); @@ -829,9 +829,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return A.That.Matches(x => x.App == app && x.User == user && !x.Archived); } - private ContentQueryContext MatchsContentContext() + private QueryContext MatchsContentContext() { - return A.That.Matches(x => x.Base.App == app && x.Base.User == user && !x.Base.Archived && x.SchemaIdOrName == schema.Id.ToString()); + return A.That.Matches(x => x.App == app && x.User == user && !x.Archived); } } }