From 91c1532ef7991d1a2ebfa08581ca93c08a396d61 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Feb 2021 14:13:25 +0100 Subject: [PATCH 1/6] Fix surrogate keys. --- .../Assets/MongoAssetRepository.cs | 2 +- .../Assets/IAssetQueryService.cs | 6 +- .../Assets/Queries/AssetQueryService.cs | 160 ++++++++++---- .../Assets/Repositories/IAssetRepository.cs | 6 +- .../Contents/Queries/ContentQueryService.cs | 73 +++---- .../Assets/AssetContentController.cs | 20 +- .../Assets/MongoDb/AssetsQueryTests.cs | 2 +- .../Assets/Queries/AssetQueryServiceTests.cs | 174 +++++++++++++--- .../Queries/ContentQueryServiceTests.cs | 196 ++++++++++-------- 9 files changed, 423 insertions(+), 216 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 729994f66..6df8b75a4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize) + public async Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize) { using (Profiler.TraceMethod()) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index 2d4945aa9..d3db4381b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -21,6 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Assets Task FindByHashAsync(Context context, string hash, string fileName, long fileSize); - Task FindAsync(Context context, DomainId id); + Task FindAsync(Context context, DomainId id, long version = EtagVersion.Any); + + Task FindBySlugAsync(Context context, string slug); + + Task FindGlobalAsync(Context context, DomainId id); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index 3919afd6b..af9d40e63 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -10,107 +10,195 @@ using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; +using Squidex.Log; namespace Squidex.Domain.Apps.Entities.Assets.Queries { public sealed class AssetQueryService : IAssetQueryService { + private static readonly IResultList EmptyAssets = ResultList.CreateFrom(0); private readonly IAssetEnricher assetEnricher; private readonly IAssetRepository assetRepository; + private readonly IAssetLoader assetLoader; private readonly IAssetFolderRepository assetFolderRepository; private readonly AssetQueryParser queryParser; public AssetQueryService( IAssetEnricher assetEnricher, IAssetRepository assetRepository, + IAssetLoader assetLoader, IAssetFolderRepository assetFolderRepository, AssetQueryParser queryParser) { Guard.NotNull(assetEnricher, nameof(assetEnricher)); Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(assetLoader, nameof(assetLoader)); Guard.NotNull(assetFolderRepository, nameof(assetFolderRepository)); Guard.NotNull(queryParser, nameof(queryParser)); this.assetEnricher = assetEnricher; this.assetRepository = assetRepository; + this.assetLoader = assetLoader; this.assetFolderRepository = assetFolderRepository; this.queryParser = queryParser; } - public async Task FindByHashAsync(Context context, string hash, string fileName, long fileSize) + public async Task> FindAssetFolderAsync(DomainId appId, DomainId id) { - Guard.NotNull(context, nameof(context)); + using (Profiler.TraceMethod()) + { + var result = new List(); - var asset = await assetRepository.FindAssetAsync(context.App.Id, hash, fileName, fileSize); + while (id != DomainId.Empty) + { + var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id); - if (asset != null) - { - return await assetEnricher.EnrichAsync(asset, context); + if (folder == null || result.Any(x => x.Id == folder.Id)) + { + result.Clear(); + break; + } + + result.Insert(0, folder); + + id = folder.ParentId; + } + + return result; } + } + + public async Task> QueryAssetFoldersAsync(Context context, DomainId parentId) + { + using (Profiler.TraceMethod()) + { + var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId); - return null; + return assetFolders; + } } - public async Task FindAsync(Context context, DomainId id) + public async Task FindByHashAsync(Context context, string hash, string fileName, long fileSize) { Guard.NotNull(context, nameof(context)); - var asset = await assetRepository.FindAssetAsync(context.App.Id, id); - - if (asset != null) + using (Profiler.TraceMethod()) { - return await assetEnricher.EnrichAsync(asset, context); - } + var asset = await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize); + + if (asset == null) + { + return null; + } - return null; + return await TransformAsync(context, asset); + } } - public async Task> FindAssetFolderAsync(DomainId appId, DomainId id) + public async Task FindBySlugAsync(Context context, string slug) { - var result = new List(); + Guard.NotNull(context, nameof(context)); - while (id != DomainId.Empty) + using (Profiler.TraceMethod()) { - var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id); + var asset = await assetRepository.FindAssetBySlugAsync(context.App.Id, slug); - if (folder == null || result.Any(x => x.Id == folder.Id)) + if (asset == null) { - result.Clear(); - break; + return null; } - result.Insert(0, folder); - - id = folder.ParentId; + return await TransformAsync(context, asset); } + } + + public async Task FindGlobalAsync(Context context, DomainId id) + { + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var asset = await assetRepository.FindAssetAsync(id); + + if (asset == null) + { + return null; + } - return result; + return await TransformAsync(context, asset); + } } - public async Task> QueryAssetFoldersAsync(Context context, DomainId parentId) + public async Task FindAsync(Context context, DomainId id, long version = EtagVersion.Any) { - var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId); + Guard.NotNull(context, nameof(context)); - return assetFolders; + using (Profiler.TraceMethod()) + { + IAssetEntity? asset; + + if (version > EtagVersion.Empty) + { + asset = await assetLoader.GetAsync(context.App.Id, id, version); + } + else + { + asset = await assetRepository.FindAssetAsync(context.App.Id, id); + } + + if (asset == null) + { + return null; + } + + return await TransformAsync(context, asset); + } } public async Task> QueryAsync(Context context, DomainId? parentId, Q q) { Guard.NotNull(context, nameof(context)); - Guard.NotNull(q, nameof(q)); - q = await queryParser.ParseQueryAsync(context, q); - - var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q); + if (q == null) + { + return EmptyAssets; + } - if (q.Ids != null && q.Ids.Count > 0) + using (Profiler.TraceMethod()) { - assets = assets.SortSet(x => x.Id, q.Ids); + q = await queryParser.ParseQueryAsync(context, q); + + var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q); + + if (q.Ids != null && q.Ids.Count > 0) + { + assets = assets.SortSet(x => x.Id, q.Ids); + } + + return await TransformAsync(context, assets); } + } - var enriched = await assetEnricher.EnrichAsync(assets, context); + private async Task> TransformAsync(Context context, IResultList assets) + { + var transformed = await TransformCoreAsync(context, assets); - return ResultList.Create(assets.Total, enriched); + return ResultList.Create(assets.Total, transformed); + } + + private async Task TransformAsync(Context context, IAssetEntity asset) + { + var transformed = await TransformCoreAsync(context, Enumerable.Repeat(asset, 1)); + + return transformed[0]; + } + + private async Task> TransformCoreAsync(Context context, IEnumerable assets) + { + using (Profiler.TraceMethod()) + { + return await assetEnricher.EnrichAsync(assets, context); + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index d50fe4bc7..ed20b6fcb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -21,12 +21,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories Task> QueryChildIdsAsync(DomainId appId, DomainId parentId); - Task FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize); + Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize); + + Task FindAssetBySlugAsync(DomainId appId, string slug); Task FindAssetAsync(DomainId appId); Task FindAssetAsync(DomainId appId, DomainId id); - - Task FindAssetBySlugAsync(DomainId appId, string slug); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index ae554b021..0424ae92e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -25,52 +25,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private readonly IAppProvider appProvider; private readonly IContentEnricher contentEnricher; private readonly IContentRepository contentRepository; - private readonly IContentLoader contentVersionLoader; + private readonly IContentLoader contentLoader; private readonly ContentQueryParser queryParser; public ContentQueryService( IAppProvider appProvider, IContentEnricher contentEnricher, IContentRepository contentRepository, - IContentLoader contentVersionLoader, + IContentLoader assetLoader, ContentQueryParser queryParser) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(contentEnricher, nameof(contentEnricher)); Guard.NotNull(contentRepository, nameof(contentRepository)); - Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader)); + Guard.NotNull(assetLoader, nameof(assetLoader)); Guard.NotNull(queryParser, nameof(queryParser)); this.appProvider = appProvider; this.contentEnricher = contentEnricher; this.contentRepository = contentRepository; - this.contentVersionLoader = contentVersionLoader; + this.contentLoader = assetLoader; this.queryParser = queryParser; this.queryParser = queryParser; } - public async Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1) + public async Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any) { Guard.NotNull(context, nameof(context)); - if (id == default) - { - throw new DomainObjectNotFoundException(id.ToString()); - } - - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - using (Profiler.TraceMethod()) { + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + IContentEntity? content; if (version > EtagVersion.Empty) { - content = await FindByVersionAsync(context, id, version); + content = await contentLoader.GetAsync(context.App.Id, id, version); } else { - content = await FindCoreAsync(context, id, schema); + content = await contentRepository.FindContentAsync(context.App, schema, id, context.Scope()); } if (content == null || content.SchemaId.Id != schema.Id) @@ -86,20 +81,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { Guard.NotNull(context, nameof(context)); - if (q == null) + using (Profiler.TraceMethod()) { - return EmptyContents; - } + if (q == null) + { + return EmptyContents; + } - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - if (!HasPermission(context, schema, Permissions.AppContentsRead)) - { - q.CreatedBy = context.User.Token(); - } + if (!HasPermission(context, schema, Permissions.AppContentsRead)) + { + q.CreatedBy = context.User.Token(); + } - using (Profiler.TraceMethod()) - { q = await queryParser.ParseAsync(context, q, schema); var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope()); @@ -117,20 +112,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { Guard.NotNull(context, nameof(context)); - if (q == null) + using (Profiler.TraceMethod()) { - return EmptyContents; - } + if (q == null) + { + return EmptyContents; + } - var schemas = await GetSchemasAsync(context); + var schemas = await GetSchemasAsync(context); - if (schemas.Count == 0) - { - return EmptyContents; - } + if (schemas.Count == 0) + { + return EmptyContents; + } - using (Profiler.TraceMethod()) - { q = await queryParser.ParseAsync(context, q); var contents = await contentRepository.QueryAsync(context.App, schemas, q, context.Scope()); @@ -218,15 +213,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { return context.Permissions.Allows(permissionId, context.App.Name, schema.SchemaDef.Name); } - - private Task FindCoreAsync(Context context, DomainId id, ISchemaEntity schema) - { - return contentRepository.FindContentAsync(context.App, schema, id, context.Scope()); - } - - private Task FindByVersionAsync(Context context, DomainId id, long version) - { - return contentVersionLoader.GetAsync(context.App.Id, id, version); - } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 947049964..820364ded 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -17,7 +17,6 @@ using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Assets; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Log; @@ -34,23 +33,20 @@ namespace Squidex.Areas.Api.Controllers.Assets public sealed class AssetContentController : ApiController { private readonly IAssetFileStore assetFileStore; - private readonly IAssetRepository assetRepository; - private readonly IAssetLoader assetLoader; + private readonly IAssetQueryService assetQuery; private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; public AssetContentController( ICommandBus commandBus, IAssetFileStore assetFileStore, - IAssetRepository assetRepository, - IAssetLoader assetLoader, + IAssetQueryService assetQuery, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator) : base(commandBus) { this.assetFileStore = assetFileStore; - this.assetRepository = assetRepository; - this.assetLoader = assetLoader; + this.assetQuery = assetQuery; this.assetStore = assetStore; this.assetThumbnailGenerator = assetThumbnailGenerator; } @@ -74,11 +70,11 @@ namespace Squidex.Areas.Api.Controllers.Assets [AllowAnonymous] public async Task GetAssetContentBySlug(string app, string idOrSlug, [FromQuery] AssetContentQueryDto queries, string? more = null) { - var asset = await assetRepository.FindAssetAsync(AppId, DomainId.Create(idOrSlug)); + var asset = await assetQuery.FindAsync(Context, DomainId.Create(idOrSlug)); if (asset == null) { - asset = await assetRepository.FindAssetBySlugAsync(AppId, idOrSlug); + asset = await assetQuery.FindBySlugAsync(Context, idOrSlug); } return await DeliverAssetAsync(asset, queries); @@ -102,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Obsolete("Use overload with app name")] public async Task GetAssetContent(DomainId id, [FromQuery] AssetContentQueryDto queries) { - var asset = await assetRepository.FindAssetAsync(id); + var asset = await assetQuery.FindGlobalAsync(Context, id); return await DeliverAssetAsync(asset, queries); } @@ -123,9 +119,9 @@ namespace Squidex.Areas.Api.Controllers.Assets return StatusCode(403); } - if (asset != null && queries.Version > EtagVersion.Any && asset.Version != queries.Version) + if (asset != null && queries.Version > EtagVersion.Any && asset.Version != queries.Version && Context.App != null) { - asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, queries.Version); + asset = await assetQuery.FindAsync(Context, asset.Id, queries.Version); } if (asset == null) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs index aeec3db70..81c0af1c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb { var random = _.RandomValue(); - var assets = await _.AssetRepository.FindAssetAsync(_.RandomAppId(), random, random, 1024); + var assets = await _.AssetRepository.FindAssetByHashAsync(_.RandomAppId(), random, random, 1024); Assert.NotNull(assets); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index ec32c9dc2..6a0ff152b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -12,6 +12,7 @@ using FakeItEasy; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets.Queries @@ -20,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { private readonly IAssetEnricher assetEnricher = A.Fake(); private readonly IAssetRepository assetRepository = A.Fake(); + private readonly IAssetLoader assetLoader = A.Fake(); private readonly IAssetFolderRepository assetFolderRepository = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly Context requestContext; @@ -30,76 +32,167 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); + SetupEnricher(); + A.CallTo(() => queryParser.ParseQueryAsync(requestContext, A._)) .ReturnsLazily(c => Task.FromResult(c.GetArgument(1)!)); - sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser); + sut = new AssetQueryService(assetEnricher, assetRepository, assetLoader, assetFolderRepository, queryParser); + } + + [Fact] + public async Task Should_find_asset_by_slug_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug")) + .Returns(asset); + + var result = await sut.FindBySlugAsync(requestContext, "slug"); + + AssertAsset(asset, result); + } + + [Fact] + public async Task Should_return_null_if_asset_by_slug_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug")) + .Returns(Task.FromResult(null)); + + var result = await sut.FindBySlugAsync(requestContext, "slug"); + + Assert.Null(result); } [Fact] public async Task Should_find_asset_by_id_and_enrich_it() { - var found = new AssetEntity { Id = DomainId.NewGuid() }; + var asset = CreateAsset(DomainId.NewGuid()); - var enriched = new AssetEntity(); + A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id)) + .Returns(asset); - A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, found.Id)) - .Returns(found); + var result = await sut.FindAsync(requestContext, asset.Id); - A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext)) - .Returns(enriched); + AssertAsset(asset, result); + } - var result = await sut.FindAsync(requestContext, found.Id); + [Fact] + public async Task Should_return_null_if_asset_by_id_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id)) + .Returns(Task.FromResult(null)); - Assert.Same(enriched, result); + var result = await sut.FindAsync(requestContext, asset.Id); + + Assert.Null(result); } [Fact] - public async Task Should_find_assets_by_hash_and_and_enrich_it() + public async Task Should_find_asset_by_id_and_version_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2)) + .Returns(asset); + + var result = await sut.FindAsync(requestContext, asset.Id, 2); + + AssertAsset(asset, result); + } + + [Fact] + public async Task Should_return_null_if_asset_by_id_and_version_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2)) + .Returns(Task.FromResult(null)); + + var result = await sut.FindAsync(requestContext, asset.Id, 2); + + Assert.Null(result); + } + + [Fact] + public async Task Should_find_global_asset_by_id_and_enrich_it() { - var found = new AssetEntity { Id = DomainId.NewGuid() }; + var asset = CreateAsset(DomainId.NewGuid()); - var enriched = new AssetEntity(); + A.CallTo(() => assetRepository.FindAssetAsync(asset.Id)) + .Returns(asset); - A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, "hash", "name", 123)) - .Returns(found); + var result = await sut.FindGlobalAsync(requestContext, asset.Id); - A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext)) - .Returns(enriched); + AssertAsset(asset, result); + } + + [Fact] + public async Task Should_return_null_if_global_asset_by_id_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetRepository.FindAssetAsync(asset.Id)) + .Returns(Task.FromResult(null)); + + var result = await sut.FindGlobalAsync(requestContext, asset.Id); + + Assert.Null(result); + } + + [Fact] + public async Task Should_find_assets_by_hash_and_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123)) + .Returns(asset); var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123); - Assert.Same(enriched, result); + AssertAsset(asset, result); } [Fact] - public async Task Should_load_assets_with_query_and_resolve_tags() + public async Task Should_return_null_if_asset_by_hash_cannot_be_found() { - var found1 = new AssetEntity { Id = DomainId.NewGuid() }; - var found2 = new AssetEntity { Id = DomainId.NewGuid() }; + var asset = CreateAsset(DomainId.NewGuid()); - var enriched1 = new AssetEntity(); - var enriched2 = new AssetEntity(); + A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123)) + .Returns(Task.FromResult(null)); + + var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123); + + Assert.Null(result); + } + + [Fact] + public async Task Should_query_assets_and_enrich_it() + { + var asset1 = CreateAsset(DomainId.NewGuid()); + var asset2 = CreateAsset(DomainId.NewGuid()); var parentId = DomainId.NewGuid(); var q = Q.Empty.WithODataQuery("fileName eq 'Name'"); A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q)) - .Returns(ResultList.CreateFrom(8, found1, found2)); - - A.CallTo(() => assetEnricher.EnrichAsync(A>.That.IsSameSequenceAs(found1, found2), requestContext)) - .Returns(new List { enriched1, enriched2 }); + .Returns(ResultList.CreateFrom(8, asset1, asset2)); var result = await sut.QueryAsync(requestContext, parentId, q); Assert.Equal(8, result.Total); - Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray()); + AssertAsset(asset1, result[0]); + AssertAsset(asset2, result[1]); } [Fact] - public async Task Should_load_assets_folders_from_repository() + public async Task Should_query_asset_folders() { var parentId = DomainId.NewGuid(); @@ -114,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries } [Fact] - public async Task Should_resolve_folder_path_from_root() + public async Task Should_find_asset_folder_with_path() { var folderId1 = DomainId.NewGuid(); var folder1 = CreateFolder(folderId1); @@ -205,6 +298,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries Assert.Empty(result); } + private static void AssertAsset(IAssetEntity source, IEnrichedAssetEntity? result) + { + Assert.NotNull(result); + Assert.NotSame(source, result); + Assert.Equal(source.AssetId, result?.AssetId); + } + private static IAssetFolderEntity CreateFolder(DomainId id, DomainId parentId = default) { var assetFolder = A.Fake(); @@ -214,5 +314,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries return assetFolder; } + + private static AssetEntity CreateAsset(DomainId id) + { + return new AssetEntity { Id = id }; + } + + private void SetupEnricher() + { + A.CallTo(() => assetEnricher.EnrichAsync(A>._, A._)) + .ReturnsLazily(x => + { + var input = x.GetArgument>(0)!; + + return Task.FromResult>(input.Select(c => SimpleMapper.Map(c, new AssetEntity())).ToList()); + }); + } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index bdfe78f1d..c7bbf0882 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -31,11 +31,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private readonly IContentRepository contentRepository = A.Fake(); private readonly IContentLoader contentVersionLoader = A.Fake(); private readonly ISchemaEntity schema; - private readonly DomainId contentId = DomainId.NewGuid(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); private readonly ContentData contentData = new ContentData(); - private readonly ContentData contentTransformed = new ContentData(); private readonly ContentQueryParser queryParser = A.Fake(); private readonly ContentQueryService sut; @@ -67,66 +65,70 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } [Fact] - public async Task GetSchemaOrThrowAsync_should_return_schema_from_guid_string() + public async Task Should_get_schema_from_guid_string() { var input = schemaId.Id.ToString(); - var ctx = CreateContext(isFrontend: false, allowSchema: true); + var requestContext = CreateContext(); A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true)) .Returns(schema); - var result = await sut.GetSchemaOrThrowAsync(ctx, input); + var result = await sut.GetSchemaOrThrowAsync(requestContext, input); Assert.Equal(schema, result); } [Fact] - public async Task GetSchemaOrThrowAsync_should_return_schema_from_name() + public async Task Should_get_schema_from_name() { var input = schemaId.Name; - var ctx = CreateContext(isFrontend: false, allowSchema: true); + var requestContext = CreateContext(); A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true)) .Returns(schema); - var result = await sut.GetSchemaOrThrowAsync(ctx, input); + var result = await sut.GetSchemaOrThrowAsync(requestContext, input); Assert.Equal(schema, result); } [Fact] - public async Task GetSchemaOrThrowAsync_should_throw_404_if_not_found() + public async Task Should_throw_notfound_exception_if_schema_to_get_not_found() { - var ctx = CreateContext(isFrontend: false, allowSchema: true); + var requestContext = CreateContext(); A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, true)) .Returns((ISchemaEntity?)null); - await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name)); } [Fact] - public async Task FindContentAsync_should_throw_exception_if_user_has_no_permission() + public async Task Should_throw_permission_exception_if_content_to_find_is_restricted() { - var ctx = CreateContext(isFrontend: false, allowSchema: false); + var requestContext = CreateContext(allowSchema: false); - A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A._)) - .Returns(CreateContent(contentId)); + var content = CreateContent(DomainId.NewGuid()); - await Assert.ThrowsAsync(() => sut.FindAsync(ctx, schemaId.Name, contentId)); + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A._)) + .Returns(CreateContent(DomainId.NewGuid())); + + await Assert.ThrowsAsync(() => sut.FindAsync(requestContext, schemaId.Name, content.Id)); } [Fact] - public async Task FindContentAsync_should_return_null_if_not_found() + public async Task Should_return_null_if_content_by_id_dannot_be_found() { - var ctx = CreateContext(isFrontend: false, allowSchema: true); + var requestContext = CreateContext(); + + var content = CreateContent(DomainId.NewGuid()); - A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A._)) + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A._)) .Returns(null); - Assert.Null(await sut.FindAsync(ctx, schemaId.Name, contentId)); + Assert.Null(await sut.FindAsync(requestContext, schemaId.Name, content.Id)); } [Theory] @@ -134,45 +136,41 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [InlineData(1, 1, SearchScope.All)] [InlineData(0, 1, SearchScope.All)] [InlineData(0, 0, SearchScope.Published)] - public async Task FindContentAsync_should_return_content(int isFrontend, int unpublished, SearchScope scope) + public async Task Should_return_content_by_id(int isFrontend, int unpublished, SearchScope scope) { - var ctx = - CreateContext(isFrontend: isFrontend == 1, allowSchema: true) - .WithUnpublished(unpublished == 1); + var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); - var content = CreateContent(contentId); + var content = CreateContent(DomainId.NewGuid()); - A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, scope)) + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, scope)) .Returns(content); - var result = await sut.FindAsync(ctx, schemaId.Name, contentId); + var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id); - Assert.Equal(contentTransformed, result!.Data); - Assert.Equal(content.Id, result.Id); + AssertContent(content, result); } [Fact] - public async Task FindContentAsync_should_return_content_by_version() + public async Task Should_return_content_by_id_and_version() { - var ctx = CreateContext(isFrontend: false, allowSchema: true); + var requestContext = CreateContext(); - var content = CreateContent(contentId); + var content = CreateContent(DomainId.NewGuid()); - A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, contentId, 13)) + A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13)) .Returns(content); - var result = await sut.FindAsync(ctx, schemaId.Name, contentId, 13); + var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13); - Assert.Equal(contentTransformed, result!.Data); - Assert.Equal(content.Id, result.Id); + AssertContent(content, result); } [Fact] - public async Task QueryAsync_should_throw_if_user_has_no_permission() + public async Task Should_throw_exception_if_user_has_no_permission_to_query_content() { - var ctx = CreateContext(isFrontend: false, allowSchema: false); + var requestContext = CreateContext(allowSchema: false); - await Assert.ThrowsAsync(() => sut.QueryAsync(ctx, schemaId.Name, Q.Empty)); + await Assert.ThrowsAsync(() => sut.QueryAsync(requestContext, schemaId.Name, Q.Empty)); } [Theory] @@ -180,89 +178,95 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [InlineData(1, 1, SearchScope.All)] [InlineData(0, 1, SearchScope.All)] [InlineData(0, 0, SearchScope.Published)] - public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope) + public async Task Should_query_contents(int isFrontend, int unpublished, SearchScope scope) { - var ctx = - CreateContext(isFrontend: isFrontend == 1, allowSchema: true) - .WithUnpublished(unpublished == 1); + var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); - var content = CreateContent(contentId); + var content1 = CreateContent(DomainId.NewGuid()); + var content2 = CreateContent(DomainId.NewGuid()); var q = Q.Empty.WithReference(DomainId.NewGuid()); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, q, scope)) - .Returns(ResultList.CreateFrom(5, content)); - - var result = await sut.QueryAsync(ctx, schemaId.Name, q); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, q, scope)) + .Returns(ResultList.CreateFrom(5, content1, content2)); - Assert.Equal(contentData, result[0].Data); - Assert.Equal(contentId, result[0].Id); + var result = await sut.QueryAsync(requestContext, schemaId.Name, q); Assert.Equal(5, result.Total); + + AssertContent(content1, result[0]); + AssertContent(content2, result[1]); } - [Fact] - public async Task QueryAll_should_not_return_contents_if_user_has_no_permission() + [Theory] + [InlineData(1, 0, SearchScope.All)] + [InlineData(1, 1, SearchScope.All)] + [InlineData(0, 1, SearchScope.All)] + [InlineData(0, 0, SearchScope.Published)] + public async Task Should_query_contents_by_ids(int isFrontend, int unpublished, SearchScope scope) { - var ctx = CreateContext(isFrontend: false, allowSchema: false); + var requestContext = CreateContext(isFrontend, isUnpublished: unpublished); var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); + var contents = ids.Select(CreateContent).ToList(); + var q = Q.Empty.WithIds(ids); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, A>.That.Matches(x => x.Count == 0), q, SearchScope.All)) - .Returns(ResultList.Create(0, ids.Select(CreateContent))); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, + A>.That.Matches(x => x.Count == 1), q, scope)) + .Returns(ResultList.Create(5, contents)); - var result = await sut.QueryAsync(ctx, q); + var result = await sut.QueryAsync(requestContext, q); - Assert.Empty(result); + Assert.Equal(5, result.Total); + + for (var i = 0; i < contents.Count; i++) + { + AssertContent(contents[i], result[i]); + } } [Fact] - public async Task QueryAll_should_only_query_only_users_contents_if_no_permission() + public async Task Should_query_contents_with_matching_permissions() { - var ctx = - CreateContext(true, true, Permissions.AppContentsReadOwn); + var requestContext = CreateContext(allowSchema: false); - await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); + var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A.That.Matches(x => x.CreatedBy!.Equals(ctx.User.Token())), SearchScope.All)) - .MustHaveHappened(); + var q = Q.Empty.WithIds(ids); + + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, + A>.That.Matches(x => x.Count == 0), q, SearchScope.All)) + .Returns(ResultList.Create(0, ids.Select(CreateContent))); + + var result = await sut.QueryAsync(requestContext, q); + + Assert.Empty(result); } [Fact] - public async Task QueryAll_should_query_all_contents_if_user_has_permission() + public async Task Should_query_contents_from_user_if_user_has_only_own_permission() { - var ctx = - CreateContext(true, true, Permissions.AppContentsRead); + var requestContext = CreateContext(permissionId: Permissions.AppContentsReadOwn); - await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); + await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A.That.Matches(x => x.CreatedBy == null), SearchScope.All)) + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, + A.That.Matches(x => Equals(x.CreatedBy, requestContext.User.Token())), SearchScope.Published)) .MustHaveHappened(); } - [Theory] - [InlineData(1, 0, SearchScope.All)] - [InlineData(1, 1, SearchScope.All)] - [InlineData(0, 1, SearchScope.All)] - [InlineData(0, 0, SearchScope.Published)] - public async Task QueryAll_should_return_contents(int isFrontend, int unpublished, SearchScope scope) + [Fact] + public async Task Should_query_all_contents_if_user_has_read_permission() { - var ctx = - CreateContext(isFrontend: isFrontend == 1, allowSchema: true) - .WithUnpublished(unpublished == 1); + var requestContext = CreateContext(permissionId: Permissions.AppContentsRead); - var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList(); - - var q = Q.Empty.WithIds(ids); - - A.CallTo(() => contentRepository.QueryAsync(ctx.App, A>.That.Matches(x => x.Count == 1), q, scope)) - .Returns(ResultList.Create(5, ids.Select(CreateContent))); - - var result = await sut.QueryAsync(ctx, q); + await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty); - Assert.Equal(ids, result.Select(x => x.Id).ToList()); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, + A.That.Matches(x => x.CreatedBy == null), SearchScope.Published)) + .MustHaveHappened(); } private void SetupEnricher() @@ -276,12 +280,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries }); } - private Context CreateContext(bool isFrontend, bool allowSchema, string permissionId = Permissions.AppContentsRead) + private Context CreateContext( + int isFrontend = 0, + int isUnpublished = 0, + bool allowSchema = true, + string permissionId = Permissions.AppContentsRead) { var claimsIdentity = new ClaimsIdentity(); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - if (isFrontend) + claimsIdentity.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + + if (isFrontend == 1) { claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); } @@ -293,7 +303,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, concretePermission)); } - return new Context(claimsPrincipal, Mocks.App(appId)); + return new Context(claimsPrincipal, Mocks.App(appId)).WithUnpublished(isUnpublished == 1); + } + + private static void AssertContent(IContentEntity source, IEnrichedContentEntity? result) + { + Assert.NotNull(result); + Assert.NotSame(source, result); + Assert.Same(source.Data, result?.Data); + Assert.Equal(source.Id, result?.Id); } private IContentEntity CreateContent(DomainId id) From 3779c038732aa8f403ef3c5c5c47c1bb5aa16510 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Feb 2021 14:15:13 +0100 Subject: [PATCH 2/6] Fallback handling. --- .../Controllers/Assets/AssetContentController.cs | 15 +++++++++++++-- .../Contents/TestData/FakeUrlGenerator.cs | 5 ----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 820364ded..7588efb6c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -34,6 +34,7 @@ namespace Squidex.Areas.Api.Controllers.Assets { private readonly IAssetFileStore assetFileStore; private readonly IAssetQueryService assetQuery; + private readonly IAssetLoader assetLoader; private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; @@ -41,12 +42,14 @@ namespace Squidex.Areas.Api.Controllers.Assets ICommandBus commandBus, IAssetFileStore assetFileStore, IAssetQueryService assetQuery, + IAssetLoader assetLoader, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator) : base(commandBus) { this.assetFileStore = assetFileStore; this.assetQuery = assetQuery; + this.assetLoader = assetLoader; this.assetStore = assetStore; this.assetThumbnailGenerator = assetThumbnailGenerator; } @@ -119,9 +122,17 @@ namespace Squidex.Areas.Api.Controllers.Assets return StatusCode(403); } - if (asset != null && queries.Version > EtagVersion.Any && asset.Version != queries.Version && Context.App != null) + if (asset != null && queries.Version > EtagVersion.Any && asset.Version != queries.Version) { - asset = await assetQuery.FindAsync(Context, asset.Id, queries.Version); + if (Context.App != null) + { + asset = await assetQuery.FindAsync(Context, asset.Id, queries.Version); + } + else + { + // Fallback for old endpoint. Does not set the surrogate key. + asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, queries.Version); + } } if (asset == null) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs index 7853dd3d0..995699c2c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs @@ -36,11 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData return $"contents/{schemaId.Name}/{contentId}"; } - public string AppSettingsUI(NamedId appId) - { - throw new NotSupportedException(); - } - public string AssetsUI(NamedId appId, string? query = null) { throw new NotSupportedException(); From 7173e95d5d633230862d20038e790baa69705b2d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Feb 2021 17:47:39 +0100 Subject: [PATCH 3/6] Script and template extensions for references and assets. --- .../Extensions/EventJintExtension.cs | 2 +- .../Scripting/ExecutionContext.cs | 45 ++++-- .../Scripting/Extensions/HttpJintExtension.cs | 21 ++- .../Scripting/IJintExtension.cs | 6 +- .../Scripting/JintScriptEngine.cs | 28 ++-- .../Scripting/ScriptContext.cs | 37 +++++ .../Scripting/ScriptVars.cs | 26 ++-- .../Assets/AssetsFluidExtension.cs | 92 +++++++++++ .../Assets/AssetsJintExtension.cs | 114 ++++++++++++++ .../Contents/Counter/CounterJintExtension.cs | 4 +- .../Contents/ReferencesFluidExtension.cs | 43 +++--- .../Contents/ReferencesJintExtension.cs | 116 ++++++++++++++ .../src/Squidex/Config/Domain/RuleServices.cs | 9 ++ .../Scripting/JintScriptEngineHelperTests.cs | 63 ++++++-- .../Assets/AssetsFluidExtensionTests.cs | 99 ++++++++++++ .../Assets/AssetsJintExtensionTests.cs | 132 ++++++++++++++++ .../ContentChangedTriggerHandlerTests.cs | 1 - ...ts.cs => ReferencesFluidExtensionTests.cs} | 36 +++-- .../Contents/ReferencesJintExtensionTests.cs | 143 ++++++++++++++++++ .../EnrichWithAppIdCommandMiddlewareTests.cs | 1 - 20 files changed, 927 insertions(+), 91 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/{ReferenceFluidExtensionTests.cs => ReferencesFluidExtensionTests.cs} (77%) create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs index c08a08a61..1536ec15a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions this.urlGenerator = urlGenerator; } - public void Extend(ExecutionContext context, bool async) + public void Extend(ExecutionContext context) { context.Engine.SetValue("contentAction", new EventDelegate(() => { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs index 226a13958..acd08d133 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs @@ -15,26 +15,19 @@ using Squidex.Text; namespace Squidex.Domain.Apps.Core.Scripting { - public delegate bool ExceptionHandler(Exception exception); - - public sealed class ExecutionContext : Dictionary + public sealed class ExecutionContext : ScriptContext { - private readonly ExceptionHandler? exceptionHandler; + private Func? completion; public Engine Engine { get; } - public CancellationToken CancellationToken { get; } + public CancellationToken CancellationToken { get; private set; } public bool IsAsync { get; private set; } - internal ExecutionContext(Engine engine, CancellationToken cancellationToken, ExceptionHandler? exceptionHandler = null) - : base(StringComparer.OrdinalIgnoreCase) + internal ExecutionContext(Engine engine) { Engine = engine; - - CancellationToken = cancellationToken; - - this.exceptionHandler = exceptionHandler; } public void MarkAsync() @@ -44,10 +37,34 @@ namespace Squidex.Domain.Apps.Core.Scripting public void Fail(Exception exception) { - exceptionHandler?.Invoke(exception); + completion?.Invoke(exception); + } + + public ExecutionContext ExtendAsync(IEnumerable extensions, Func completion, CancellationToken ct) + { + CancellationToken = ct; + + this.completion = completion; + + foreach (var extension in extensions) + { + extension.ExtendAsync(this); + } + + return this; + } + + public ExecutionContext Extend(IEnumerable extensions) + { + foreach (var extension in extensions) + { + extension.Extend(this); + } + + return this; } - public void AddVariables(ScriptVars vars, ScriptOptions options) + public ExecutionContext Extend(ScriptVars vars, ScriptOptions options) { var engine = Engine; @@ -86,6 +103,8 @@ namespace Squidex.Domain.Apps.Core.Scripting } engine.SetValue("async", true); + + return this; } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs index 1e805ae3b..8f25001ad 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs @@ -29,14 +29,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions this.httpClientFactory = httpClientFactory; } - public void Extend(ExecutionContext context, bool async) + public void ExtendAsync(ExecutionContext context) { - if (async) - { - var engine = context.Engine; + var action = new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers)); - engine.SetValue("getJSON", new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers))); - } + context.Engine.SetValue("getJSON", action); } private void GetJson(ExecutionContext context, string url, Action callback, JsValue? headers) @@ -46,6 +43,18 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions private async Task GetJsonAsync(ExecutionContext context, string url, Action callback, JsValue? headers) { + if (callback == null) + { + context.Fail(new JavaScriptException("Callback cannot be null.")); + return; + } + + if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) + { + context.Fail(new JavaScriptException("URL is not valid.")); + return; + } + context.MarkAsync(); try diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs index 8b1a75102..4737551b3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs @@ -15,7 +15,11 @@ namespace Squidex.Domain.Apps.Core.Scripting { } - void Extend(ExecutionContext context, bool async) + void Extend(ExecutionContext context) + { + } + + void ExtendAsync(ExecutionContext context) { } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index b8b2912a2..69d05f9d2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -52,7 +52,11 @@ namespace Squidex.Domain.Apps.Core.Scripting using (cts.Token.Register(() => tcs.TrySetCanceled())) { - var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token); + var context = + CreateEngine(options) + .Extend(vars, options) + .Extend(extensions) + .ExtendAsync(extensions, tcs.TrySetException, cts.Token); context.Engine.SetValue("complete", new Action(value => { @@ -82,7 +86,11 @@ namespace Squidex.Domain.Apps.Core.Scripting using (cts.Token.Register(() => tcs.TrySetCanceled())) { - var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token); + var context = + CreateEngine(options) + .Extend(vars, options) + .Extend(extensions) + .ExtendAsync(extensions, tcs.TrySetException, cts.Token); context.Engine.SetValue("complete", new Action(_ => { @@ -126,14 +134,17 @@ namespace Squidex.Domain.Apps.Core.Scripting Guard.NotNull(vars, nameof(vars)); Guard.NotNullOrEmpty(script, nameof(script)); - var context = CreateEngine(vars, options); + var context = + CreateEngine(options) + .Extend(vars, options) + .Extend(extensions); Execute(context.Engine, script); return JsonMapper.Map(context.Engine.GetCompletionValue()); } - private ExecutionContext CreateEngine(ScriptVars vars, ScriptOptions options, ExceptionHandler? exceptionHandler = null, bool async = false, CancellationToken ct = default) + private ExecutionContext CreateEngine(ScriptOptions options) { var engine = new Engine(engineOptions => { @@ -158,14 +169,7 @@ namespace Squidex.Domain.Apps.Core.Scripting extension.Extend(engine); } - var context = new ExecutionContext(engine, ct, exceptionHandler); - - context.AddVariables(vars, options); - - foreach (var extension in extensions) - { - extension.Extend(context, async); - } + var context = new ExecutionContext(engine); return context; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs new file mode 100644 index 000000000..cde181bac --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public class ScriptContext : Dictionary + { + public ScriptContext() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out T value) + { + Guard.NotNull(key, nameof(key)); + + value = default!; + + if (TryGetValue(key, out var temp) && temp is T typed) + { + value = typed; + return true; + } + + return false; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs index ac10430e3..fa8adda24 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs @@ -6,21 +6,17 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Security.Claims; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; +#pragma warning disable CS0618 // Type or member is obsolete + namespace Squidex.Domain.Apps.Core.Scripting { - public sealed class ScriptVars : Dictionary + public sealed class ScriptVars : ScriptContext { - public ScriptVars() - : base(StringComparer.OrdinalIgnoreCase) - { - } - public ClaimsPrincipal? User { get => GetValue(); @@ -68,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Scripting get => GetValue(); set { - SetValue(value, "oldData"); + SetValue(value, nameof(OldData)); SetValue(value); } } @@ -78,11 +74,23 @@ namespace Squidex.Domain.Apps.Core.Scripting get => GetValue(); set { - SetValue(value, "oldStatus"); + SetValue(value, nameof(OldStatus)); SetValue(value); } } + [Obsolete("Use dataOld")] + public ContentData? OldData + { + get => GetValue(); + } + + [Obsolete("Use statusOld")] + public Status? OldStatus + { + get => GetValue(); + } + public void SetValue(object? value, [CallerMemberName] string? key = null) { if (key != null) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs new file mode 100644 index 000000000..d0fbdacd9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Fluid; +using Fluid.Ast; +using Fluid.Tags; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Templates; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetsFluidExtension : IFluidExtension + { + private readonly IAppProvider appProvider; + private readonly IAssetQueryService assetQuery; + + private sealed class AssetTag : ArgumentsTag + { + private readonly AssetsFluidExtension root; + + public AssetTag(AssetsFluidExtension root) + { + this.root = root; + } + + public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) + { + if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) + { + var app = await root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + + if (app == null) + { + return Completion.Normal; + } + + var requestContext = + Context.Admin(app).Clone(b => b + .WithoutTotal()); + + var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); + + var asset = await root.assetQuery.FindAsync(requestContext, DomainId.Create(id)); + + if (asset != null) + { + var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); + + context.SetValue(name, asset); + } + } + + return Completion.Normal; + } + } + + public AssetsFluidExtension(IAppProvider appProvider, IAssetQueryService assetQuery) + { + Guard.NotNull(assetQuery, nameof(assetQuery)); + Guard.NotNull(appProvider, nameof(appProvider)); + + this.assetQuery = assetQuery; + + this.appProvider = appProvider; + } + + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + } + + public void RegisterLanguageExtensions(FluidParserFactory factory) + { + factory.RegisterTag("asset", new AssetTag(this)); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs new file mode 100644 index 000000000..971ec9845 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Jint.Native; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetsJintExtension : IJintExtension + { + private delegate void GetAssetsDelegate(JsValue references, Action callback); + private readonly IAppProvider appProvider; + private readonly IAssetQueryService assetQuery; + + public AssetsJintExtension(IAppProvider appProvider, IAssetQueryService assetQuery) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(assetQuery, nameof(assetQuery)); + + this.appProvider = appProvider; + + this.assetQuery = assetQuery; + } + + public void ExtendAsync(ExecutionContext context) + { + if (!context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) + { + return; + } + + if (!context.TryGetValue(nameof(ScriptVars.User), out var user)) + { + return; + } + + var action = new GetAssetsDelegate((references, callback) => GetReferences(context, appId, user, references, callback)); + + context.Engine.SetValue("getAsset", action); + context.Engine.SetValue("getAssets", action); + } + + private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + GetReferencesAsync(context, appId, user, references, callback).Forget(); + } + + private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + Guard.NotNull(callback, nameof(callback)); + + var ids = new List(); + + if (references.IsString()) + { + ids.Add(DomainId.Create(references.ToString())); + } + else if (references.IsArray()) + { + foreach (var value in references.AsArray()) + { + if (value.IsString()) + { + ids.Add(DomainId.Create(value.ToString())); + } + } + } + + if (ids.Count == 0) + { + var emptyAssets = Array.Empty(); + + callback(JsValue.FromObject(context.Engine, emptyAssets)); + return; + } + + context.MarkAsync(); + + try + { + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + throw new JavaScriptException("App does not exist."); + } + + var requestContext = + new Context(user, app).Clone(b => b + .WithoutTotal()); + + var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids)); + + callback(JsValue.FromObject(context.Engine, assets.ToArray())); + } + catch (Exception ex) + { + context.Fail(ex); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index e3d81eabc..e28bf645c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs @@ -24,9 +24,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter this.grainFactory = grainFactory; } - public void Extend(ExecutionContext context, bool async) + public void ExtendAsync(ExecutionContext context) { - if (context.TryGetValue("appId", out var temp) && temp is DomainId appId) + if (context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) { var engine = context.Engine; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index 6c77233c5..1d624c017 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs @@ -23,50 +23,48 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ReferencesFluidExtension : IFluidExtension { - private readonly IContentQueryService contentQueryService; private readonly IAppProvider appProvider; + private readonly IContentQueryService contentQuery; private sealed class ReferenceTag : ArgumentsTag { - private readonly IContentQueryService contentQueryService; - private readonly IAppProvider appProvider; + private readonly ReferencesFluidExtension root; - public ReferenceTag(IContentQueryService contentQueryService, IAppProvider appProvider) + public ReferenceTag(ReferencesFluidExtension root) { - this.contentQueryService = contentQueryService; - - this.appProvider = appProvider; + this.root = root; } public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) { if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - var app = await appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + var app = await root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); if (app == null) { return Completion.Normal; } - var appContext = Context.Admin(app).Clone(b => b - .WithoutContentEnrichment() - .WithoutCleanup() - .WithUnpublished()); + var requestContext = + Context.Admin(app).Clone(b => b + .WithoutContentEnrichment() + .WithUnpublished() + .WithoutTotal()); var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); var domainId = DomainId.Create(id); var domainIds = new List { domainId }; - var references = await contentQueryService.QueryAsync(appContext, Q.Empty.WithIds(domainIds)); - var reference = references.FirstOrDefault(); + var contents = await root.contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds)); + var content = contents.FirstOrDefault(); - if (reference != null) + if (content != null) { var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); - context.SetValue(name, reference); + context.SetValue(name, content); } } @@ -74,12 +72,12 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public ReferencesFluidExtension(IContentQueryService contentQueryService, IAppProvider appProvider) + public ReferencesFluidExtension(IAppProvider appProvider, IContentQueryService contentQuery) { - Guard.NotNull(contentQueryService, nameof(contentQueryService)); + Guard.NotNull(contentQuery, nameof(contentQuery)); Guard.NotNull(appProvider, nameof(appProvider)); - this.contentQueryService = contentQueryService; + this.contentQuery = contentQuery; this.appProvider = appProvider; } @@ -87,11 +85,16 @@ namespace Squidex.Domain.Apps.Entities.Contents public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) { memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); + memberAccessStrategy.Register(); } public void RegisterLanguageExtensions(FluidParserFactory factory) { - factory.RegisterTag("reference", new ReferenceTag(contentQueryService, appProvider)); + factory.RegisterTag("reference", new ReferenceTag(this)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs new file mode 100644 index 000000000..7fc6428a3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Jint.Native; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ReferencesJintExtension : IJintExtension + { + private delegate void GetReferencesDelegate(JsValue references, Action callback); + private readonly IAppProvider appProvider; + private readonly IContentQueryService contentQuery; + + public ReferencesJintExtension(IAppProvider appProvider, IContentQueryService contentQuery) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(contentQuery, nameof(contentQuery)); + + this.appProvider = appProvider; + + this.contentQuery = contentQuery; + } + + public void ExtendAsync(ExecutionContext context) + { + if (!context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) + { + return; + } + + if (!context.TryGetValue(nameof(ScriptVars.User), out var user)) + { + return; + } + + var action = new GetReferencesDelegate((references, callback) => GetReferences(context, appId, user, references, callback)); + + context.Engine.SetValue("getReference", action); + context.Engine.SetValue("getReferences", action); + } + + private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + GetReferencesAsync(context, appId, user, references, callback).Forget(); + } + + private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) + { + Guard.NotNull(callback, nameof(callback)); + + var ids = new List(); + + if (references.IsString()) + { + ids.Add(DomainId.Create(references.ToString())); + } + else if (references.IsArray()) + { + foreach (var value in references.AsArray()) + { + if (value.IsString()) + { + ids.Add(DomainId.Create(value.ToString())); + } + } + } + + if (ids.Count == 0) + { + var emptyContents = Array.Empty(); + + callback(JsValue.FromObject(context.Engine, emptyContents)); + return; + } + + context.MarkAsync(); + + try + { + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + throw new JavaScriptException("App does not exist."); + } + + var requestContext = + new Context(user, app).Clone(b => b + .WithoutContentEnrichment() + .WithUnpublished() + .WithoutTotal()); + + var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids)); + + callback(JsValue.FromObject(context.Engine, contents.ToArray())); + } + catch (Exception ex) + { + context.Fail(ex); + } + } + } +} diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index 5b92340ff..9b0571d05 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -47,9 +47,18 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs index 9c5bc06ff..2bfd06608 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -11,6 +11,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using FakeItEasy; +using Jint.Runtime; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Scripting; @@ -195,7 +196,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanReject = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.NotEmpty(ex.Errors); } @@ -212,7 +215,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanReject = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.Equal("Not valid", ex.Errors.Single().Message); } @@ -229,7 +234,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanDisallow = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.Equal("Script has forbidden the operation.", ex.Message); } @@ -246,25 +253,57 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting CanDisallow = true }; - var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(new ScriptVars(), script, options)); + var vars = new ScriptVars(); + + var ex = await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script, options)); Assert.Equal("Operation not allowed", ex.Message); } + [Fact] + public async Task Should_throw_exception_when_getJson_url_is_null() + { + const string script = @" + getJSON(null, function(result) { + complete(result); + }); + "; + + var vars = new ScriptVars(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script)); + } + + [Fact] + public async Task Should_throw_exception_when_getJson_callback_is_null() + { + const string script = @" + var url = 'http://squidex.io'; + + getJSON(url, null); + "; + + var vars = new ScriptVars(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script)); + } + [Fact] public async Task Should_make_json_request() { var httpHandler = SetupRequest(); const string script = @" - async = true; + var url = 'http://squidex.io'; - getJSON('http://squidex.io', function(result) { + getJSON(url, function(result) { complete(result); }); "; - var result = await sut.ExecuteAsync(new ScriptVars(), script); + var vars = new ScriptVars(); + + var result = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeUrl("http://squidex.io/"); @@ -280,19 +319,21 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting var httpHandler = SetupRequest(); const string script = @" - async = true; - var headers = { 'X-Header1': 1, 'X-Header2': '2' }; - getJSON('http://squidex.io', function(result) { + var url = 'http://squidex.io'; + + getJSON(url, function(result) { complete(result); }, headers); "; - var result = await sut.ExecuteAsync(new ScriptVars(), script); + var vars = new ScriptVars(); + + var result = await sut.ExecuteAsync(vars, script); httpHandler.ShouldBeMethod(HttpMethod.Get); httpHandler.ShouldBeUrl("http://squidex.io/"); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs new file mode 100644 index 000000000..3add1b614 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Templates; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetsFluidExtensionTests + { + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly FluidTemplateEngine sut; + + public AssetsFluidExtensionTests() + { + var extensions = new IFluidExtension[] + { + new AssetsFluidExtension(appProvider, assetQuery) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + .Returns(Mocks.App(appId)); + + sut = new FluidTemplateEngine(extensions); + } + + [Fact] + public async Task Should_resolve_assets_in_loop() + { + var assetId1 = DomainId.NewGuid(); + var asset1 = CreateAsset(assetId1, 1); + var assetId2 = DomainId.NewGuid(); + var asset2 = CreateAsset(assetId2, 2); + + var @event = new EnrichedContentEvent + { + Data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(assetId1, assetId2))), + AppId = appId + }; + + A.CallTo(() => assetQuery.FindAsync(A._, assetId1, EtagVersion.Any)) + .Returns(asset1); + + A.CallTo(() => assetQuery.FindAsync(A._, assetId2, EtagVersion.Any)) + .Returns(asset2); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + {% for id in event.data.assets.iv %} + {% asset 'ref', id %} + Text: {{ ref.fileName }} {{ ref.id }} + {% endfor %} + "; + + var expected = $@" + Text: file1.jpg {assetId1} + Text: file2.jpg {assetId2} + "; + + var result = await sut.RenderAsync(template, vars); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index) + { + return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs new file mode 100644 index 000000000..a5229d320 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetsJintExtensionTests : IClassFixture + { + private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly JintScriptEngine sut; + + public AssetsJintExtensionTests() + { + var extensions = new IJintExtension[] + { + new AssetsJintExtension(appProvider, assetQuery) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + .Returns(Mocks.App(appId)); + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions); + } + + [Fact] + public async Task Should_resolve_asset() + { + var assetId1 = DomainId.NewGuid(); + var asset1 = CreateAsset(assetId1, 1); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(assetId1))); + + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId1))) + .Returns(ResultList.CreateFrom(1, asset1)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getAsset(data.assets.iv[0], function (assets) { + var result1 = `Text: ${assets[0].fileName}`; + + complete(`${result1}`); + })"; + + var expected = @" + Text: file1.jpg + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_resolve_assets() + { + var assetId1 = DomainId.NewGuid(); + var asset1 = CreateAsset(assetId1, 1); + var assetId2 = DomainId.NewGuid(); + var asset2 = CreateAsset(assetId1, 2); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(assetId1, assetId2))); + + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId1, assetId2))) + .Returns(ResultList.CreateFrom(2, asset1, asset2)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getAssets(data.assets.iv, function (assets) { + var result1 = `Text: ${assets[0].fileName}`; + var result2 = `Text: ${assets[1].fileName}`; + + complete(`${result1}\n${result2}`); + })"; + + var expected = @" + Text: file1.jpg + Text: file2.jpg + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index) + { + return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index c772187cc..1ddddda43 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -11,7 +11,6 @@ using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using FakeItEasy; -using Squidex.Caching; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs similarity index 77% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs index 01cd6b3a5..ee3acb642 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs @@ -17,18 +17,18 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents { - public class ReferenceFluidExtensionTests + public class ReferencesFluidExtensionTests { private readonly IContentQueryService contentQuery = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly FluidTemplateEngine sut; - public ReferenceFluidExtensionTests() + public ReferencesFluidExtensionTests() { var extensions = new IFluidExtension[] { - new ReferencesFluidExtension(contentQuery, appProvider) + new ReferencesFluidExtension(appProvider, contentQuery) }; A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var referenceId1 = DomainId.NewGuid(); var reference1 = CreateReference(referenceId1, 1); var referenceId2 = DomainId.NewGuid(); - var reference2 = CreateReference(referenceId1, 2); + var reference2 = CreateReference(referenceId2, 2); var @event = new EnrichedContentEvent { @@ -67,20 +67,20 @@ namespace Squidex.Domain.Apps.Entities.Contents }; var template = @" -{% for id in event.data.references.iv %} - {% reference 'ref', id %} - Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} -{% endfor %} -"; + {% for id in event.data.references.iv %} + {% reference 'ref', id %} + Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }} + {% endfor %} + "; - var expected = @" - Text: Hello 1 World 1 - Text: Hello 2 World 2 -"; + var expected = $@" + Text: Hello 1 World 1 {referenceId1} + Text: Hello 2 World 2 {referenceId2} + "; var result = await sut.RenderAsync(template, vars); - Assert.Equal(expected, result); + Assert.Equal(Cleanup(expected), Cleanup(result)); } private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index) @@ -98,5 +98,13 @@ namespace Squidex.Domain.Apps.Entities.Contents Id = referenceId }; } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs new file mode 100644 index 000000000..90371b471 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs @@ -0,0 +1,143 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ReferencesJintExtensionTests : IClassFixture + { + private readonly IContentQueryService contentQuery = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly JintScriptEngine sut; + + public ReferencesJintExtensionTests() + { + var extensions = new IJintExtension[] + { + new ReferencesJintExtension(appProvider, contentQuery) + }; + + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + .Returns(Mocks.App(appId)); + + sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions); + } + + [Fact] + public async Task Should_resolve_reference() + { + var referenceId1 = DomainId.NewGuid(); + var reference1 = CreateReference(referenceId1, 1); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(referenceId1))); + + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceId1))) + .Returns(ResultList.CreateFrom(1, reference1)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getReference(data.references.iv[0], function (references) { + var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; + + complete(`${result1}`); + })"; + + var expected = @" + Text: Hello 1 World 1 + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_resolve_references() + { + var referenceId1 = DomainId.NewGuid(); + var reference1 = CreateReference(referenceId1, 1); + var referenceId2 = DomainId.NewGuid(); + var reference2 = CreateReference(referenceId1, 2); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddJsonValue(JsonValue.Array(referenceId1, referenceId2))); + + A.CallTo(() => contentQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), A.That.HasIds(referenceId1, referenceId2))) + .Returns(ResultList.CreateFrom(2, reference1, reference2)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getReferences(data.references.iv, function (references) { + var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`; + var result2 = `Text: ${references[1].data.field1.iv} ${references[1].data.field2.iv}`; + + complete(`${result1}\n${result2}`); + })"; + + var expected = @" + Text: Hello 1 World 1 + Text: Hello 2 World 2 + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index) + { + return new ContentEntity + { + Data = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddJsonValue(JsonValue.Create($"Hello {index}"))) + .AddField("field2", + new ContentFieldData() + .AddJsonValue(JsonValue.Create($"World {index}"))), + Id = referenceId + }; + } + + private static string Cleanup(string text) + { + return text + .Replace("\r", string.Empty) + .Replace("\n", string.Empty) + .Replace(" ", string.Empty); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index d5eecf1df..9557d774a 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -9,7 +9,6 @@ using System; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; From e0d43afa85523cf6bc3243a1c5920c8050d9bbb1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Feb 2021 17:52:52 +0100 Subject: [PATCH 4/6] Bugfix. --- .../Contents/Counter/CounterJintExtension.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index e28bf645c..322ea9d8b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter this.grainFactory = grainFactory; } - public void ExtendAsync(ExecutionContext context) + public void Extend(ExecutionContext context) { if (context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) { From 84493107d26c5091812bf4d319f6e6b6d23dda93 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Feb 2021 20:17:08 +0100 Subject: [PATCH 5/6] Lazy references. --- .../Assets/AssetsFluidExtension.cs | 36 +++++++++++------- .../Assets/AssetsJintExtension.cs | 37 ++++++++++++------- .../Contents/ReferencesFluidExtension.cs | 36 +++++++++++------- .../Contents/ReferencesJintExtension.cs | 37 ++++++++++++------- .../Assets/AssetsFluidExtensionTests.cs | 9 ++++- .../Assets/AssetsJintExtensionTests.cs | 9 ++++- .../Contents/ReferencesFluidExtensionTests.cs | 9 ++++- .../Contents/ReferencesJintExtensionTests.cs | 9 ++++- 8 files changed, 122 insertions(+), 60 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs index d0fbdacd9..6ecd04860 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs @@ -5,38 +5,40 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; using Fluid; using Fluid.Ast; using Fluid.Tags; +using GraphQL.Utilities; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Templates; using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Assets { public sealed class AssetsFluidExtension : IFluidExtension { - private readonly IAppProvider appProvider; - private readonly IAssetQueryService assetQuery; + private readonly IServiceProvider serviceProvider; private sealed class AssetTag : ArgumentsTag { - private readonly AssetsFluidExtension root; + private readonly IServiceProvider serviceProvider; - public AssetTag(AssetsFluidExtension root) + public AssetTag(IServiceProvider serviceProvider) { - this.root = root; + this.serviceProvider = serviceProvider; } public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) { if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - var app = await root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + var app = await GetAppAsync(enrichedEvent); if (app == null) { @@ -49,7 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Assets var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); - var asset = await root.assetQuery.FindAsync(requestContext, DomainId.Create(id)); + var assetQuery = serviceProvider.GetRequiredService(); + + var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(id)); if (asset != null) { @@ -61,16 +65,20 @@ namespace Squidex.Domain.Apps.Entities.Assets return Completion.Normal; } + + private Task GetAppAsync(EnrichedEvent enrichedEvent) + { + var appProvider = serviceProvider.GetRequiredService(); + + return appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + } } - public AssetsFluidExtension(IAppProvider appProvider, IAssetQueryService assetQuery) + public AssetsFluidExtension(IServiceProvider serviceProvider) { - Guard.NotNull(assetQuery, nameof(assetQuery)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.assetQuery = assetQuery; + Guard.NotNull(serviceProvider, nameof(serviceProvider)); - this.appProvider = appProvider; + this.serviceProvider = serviceProvider; } public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) @@ -86,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public void RegisterLanguageExtensions(FluidParserFactory factory) { - factory.RegisterTag("asset", new AssetTag(this)); + factory.RegisterTag("asset", new AssetTag(serviceProvider)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs index 971ec9845..4df5836ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -10,9 +10,11 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using GraphQL.Utilities; using Jint.Native; using Jint.Runtime; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Tasks; @@ -21,17 +23,13 @@ namespace Squidex.Domain.Apps.Entities.Assets public sealed class AssetsJintExtension : IJintExtension { private delegate void GetAssetsDelegate(JsValue references, Action callback); - private readonly IAppProvider appProvider; - private readonly IAssetQueryService assetQuery; + private readonly IServiceProvider serviceProvider; - public AssetsJintExtension(IAppProvider appProvider, IAssetQueryService assetQuery) + public AssetsJintExtension(IServiceProvider serviceProvider) { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(assetQuery, nameof(assetQuery)); + Guard.NotNull(serviceProvider, nameof(serviceProvider)); - this.appProvider = appProvider; - - this.assetQuery = assetQuery; + this.serviceProvider = serviceProvider; } public void ExtendAsync(ExecutionContext context) @@ -90,17 +88,14 @@ namespace Squidex.Domain.Apps.Entities.Assets try { - var app = await appProvider.GetAppAsync(appId); - - if (app == null) - { - throw new JavaScriptException("App does not exist."); - } + var app = await GetAppAsync(appId); var requestContext = new Context(user, app).Clone(b => b .WithoutTotal()); + var assetQuery = serviceProvider.GetRequiredService(); + var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids)); callback(JsValue.FromObject(context.Engine, assets.ToArray())); @@ -110,5 +105,19 @@ namespace Squidex.Domain.Apps.Entities.Assets context.Fail(ex); } } + + private async Task GetAppAsync(DomainId appId) + { + var appProvider = serviceProvider.GetRequiredService(); + + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + throw new JavaScriptException("App does not exist."); + } + + return app; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index 1d624c017..adbb034ab 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -13,8 +14,10 @@ using System.Threading.Tasks; using Fluid; using Fluid.Ast; using Fluid.Tags; +using GraphQL.Utilities; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Templates; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; #pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections @@ -23,23 +26,22 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ReferencesFluidExtension : IFluidExtension { - private readonly IAppProvider appProvider; - private readonly IContentQueryService contentQuery; + private readonly IServiceProvider serviceProvider; private sealed class ReferenceTag : ArgumentsTag { - private readonly ReferencesFluidExtension root; + private readonly IServiceProvider serviceProvider; - public ReferenceTag(ReferencesFluidExtension root) + public ReferenceTag(IServiceProvider serviceProvider) { - this.root = root; + this.serviceProvider = serviceProvider; } public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) { if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) { - var app = await root.appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + var app = await GetAppAsync(enrichedEvent); if (app == null) { @@ -57,7 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var domainId = DomainId.Create(id); var domainIds = new List { domainId }; - var contents = await root.contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds)); + var contentQuery = serviceProvider.GetRequiredService(); + + var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds)); var content = contents.FirstOrDefault(); if (content != null) @@ -70,16 +74,20 @@ namespace Squidex.Domain.Apps.Entities.Contents return Completion.Normal; } + + private Task GetAppAsync(EnrichedEvent enrichedEvent) + { + var appProvider = serviceProvider.GetRequiredService(); + + return appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); + } } - public ReferencesFluidExtension(IAppProvider appProvider, IContentQueryService contentQuery) + public ReferencesFluidExtension(IServiceProvider serviceProvider) { - Guard.NotNull(contentQuery, nameof(contentQuery)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.contentQuery = contentQuery; + Guard.NotNull(serviceProvider, nameof(serviceProvider)); - this.appProvider = appProvider; + this.serviceProvider = serviceProvider; } public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) @@ -94,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public void RegisterLanguageExtensions(FluidParserFactory factory) { - factory.RegisterTag("reference", new ReferenceTag(this)); + factory.RegisterTag("reference", new ReferenceTag(serviceProvider)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs index 7fc6428a3..7eb69f12a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs @@ -12,7 +12,9 @@ using System.Security.Claims; using System.Threading.Tasks; using Jint.Native; using Jint.Runtime; +using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Tasks; @@ -21,17 +23,13 @@ namespace Squidex.Domain.Apps.Entities.Contents public sealed class ReferencesJintExtension : IJintExtension { private delegate void GetReferencesDelegate(JsValue references, Action callback); - private readonly IAppProvider appProvider; - private readonly IContentQueryService contentQuery; + private readonly IServiceProvider serviceProvider; - public ReferencesJintExtension(IAppProvider appProvider, IContentQueryService contentQuery) + public ReferencesJintExtension(IServiceProvider serviceProvider) { - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(contentQuery, nameof(contentQuery)); + Guard.NotNull(serviceProvider, nameof(serviceProvider)); - this.appProvider = appProvider; - - this.contentQuery = contentQuery; + this.serviceProvider = serviceProvider; } public void ExtendAsync(ExecutionContext context) @@ -90,12 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Contents try { - var app = await appProvider.GetAppAsync(appId); - - if (app == null) - { - throw new JavaScriptException("App does not exist."); - } + var app = await GetAppAsync(appId); var requestContext = new Context(user, app).Clone(b => b @@ -103,6 +96,8 @@ namespace Squidex.Domain.Apps.Entities.Contents .WithUnpublished() .WithoutTotal()); + var contentQuery = serviceProvider.GetRequiredService(); + var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids)); callback(JsValue.FromObject(context.Engine, contents.ToArray())); @@ -112,5 +107,19 @@ namespace Squidex.Domain.Apps.Entities.Contents context.Fail(ex); } } + + private async Task GetAppAsync(DomainId appId) + { + var appProvider = serviceProvider.GetRequiredService(); + + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + throw new JavaScriptException("App does not exist."); + } + + return app; + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index 3add1b614..7ff9b7291 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Templates; @@ -26,9 +27,15 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetsFluidExtensionTests() { + var services = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(assetQuery) + .BuildServiceProvider(); + var extensions = new IFluidExtension[] { - new AssetsFluidExtension(appProvider, assetQuery) + new AssetsFluidExtension(services) }; A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index a5229d320..20ede6483 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; @@ -29,9 +30,15 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetsJintExtensionTests() { + var services = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(assetQuery) + .BuildServiceProvider(); + var extensions = new IJintExtension[] { - new AssetsJintExtension(appProvider, assetQuery) + new AssetsJintExtension(services) }; A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs index ee3acb642..37875c882 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Templates; @@ -26,9 +27,15 @@ namespace Squidex.Domain.Apps.Entities.Contents public ReferencesFluidExtensionTests() { + var services = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(contentQuery) + .BuildServiceProvider(); + var extensions = new IFluidExtension[] { - new ReferencesFluidExtension(appProvider, contentQuery) + new ReferencesFluidExtension(services) }; A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs index 90371b471..5fc9e81da 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting; @@ -29,9 +30,15 @@ namespace Squidex.Domain.Apps.Entities.Contents public ReferencesJintExtensionTests() { + var services = + new ServiceCollection() + .AddSingleton(appProvider) + .AddSingleton(contentQuery) + .BuildServiceProvider(); + var extensions = new IJintExtension[] { - new ReferencesJintExtension(appProvider, contentQuery) + new ReferencesJintExtension(services) }; A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) From 0f966aa09030e4f19aead3fc65716ce13c379f6c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Feb 2021 21:10:15 +0100 Subject: [PATCH 6/6] Disable timeout. --- .../Scripting/JintScriptEngine.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 69d05f9d2..ae80d3671 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -34,6 +35,32 @@ namespace Squidex.Domain.Apps.Core.Scripting public TimeSpan TimeoutExecution { get; set; } = TimeSpan.FromMilliseconds(4000); + private TimeSpan ActualTimeoutScript + { + get + { + if (Debugger.IsAttached) + { + return TimeSpan.FromHours(1); + } + + return TimeoutScript; + } + } + + private TimeSpan ActualTimeoutExecution + { + get + { + if (Debugger.IsAttached) + { + return TimeSpan.FromHours(1); + } + + return TimeoutExecution; + } + } + public JintScriptEngine(IMemoryCache cache, IEnumerable? extensions = null) { parser = new Parser(cache); @@ -46,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Scripting Guard.NotNull(vars, nameof(vars)); Guard.NotNullOrEmpty(script, nameof(script)); - using (var cts = new CancellationTokenSource(TimeoutExecution)) + using (var cts = new CancellationTokenSource(ActualTimeoutExecution)) { var tcs = new TaskCompletionSource(); @@ -80,7 +107,7 @@ namespace Squidex.Domain.Apps.Core.Scripting Guard.NotNull(vars, nameof(vars)); Guard.NotNullOrEmpty(script, nameof(script)); - using (var cts = new CancellationTokenSource(TimeoutExecution)) + using (var cts = new CancellationTokenSource(ActualTimeoutExecution)) { var tcs = new TaskCompletionSource(); @@ -151,7 +178,7 @@ namespace Squidex.Domain.Apps.Core.Scripting engineOptions.AddObjectConverter(DefaultConverter.Instance); engineOptions.SetReferencesResolver(NullPropagation.Instance); engineOptions.Strict(); - engineOptions.TimeoutInterval(TimeoutScript); + engineOptions.TimeoutInterval(ActualTimeoutScript); }); if (options.CanDisallow)