From 7480d947c07ec05a62febd7b5c813e04ba76f9ed Mon Sep 17 00:00:00 2001 From: Paul Astbury-Thomas Date: Mon, 19 Aug 2024 10:20:43 +0100 Subject: [PATCH 1/4] Fix typo in Migrator.cs lock log message (#1120) --- backend/src/Squidex.Infrastructure/Migrations/Migrator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs index 7b46b8791..c702b2619 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -86,7 +86,7 @@ public sealed class Migrator { while (!await migrationStatus.TryLockAsync(ct)) { - log.LogInformation("Could not acquire lock to start migrating. Tryping again in {time}ms.", LockWaitMs); + log.LogInformation("Could not acquire lock to start migrating. Trying again in {time}ms.", LockWaitMs); await Task.Delay(LockWaitMs, ct); } } From 3aaa472f77e7beff47d68f60d92568ffd023ab1e Mon Sep 17 00:00:00 2001 From: Joel Arrechea Date: Mon, 19 Aug 2024 11:21:02 +0200 Subject: [PATCH 2/4] Added nodePort as an attribute of the Helm chart. (#1117) Co-authored-by: Joel --- helm/squidex/templates/service.yaml | 3 +++ helm/squidex/values.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/helm/squidex/templates/service.yaml b/helm/squidex/templates/service.yaml index 6bab1add0..dab6ce9c7 100644 --- a/helm/squidex/templates/service.yaml +++ b/helm/squidex/templates/service.yaml @@ -11,5 +11,8 @@ spec: targetPort: http protocol: TCP name: http + {{- if and (eq (lower .Values.service.type) "nodeport") .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} selector: {{- include "squidex.selectors" . | indent 4 }} \ No newline at end of file diff --git a/helm/squidex/values.yaml b/helm/squidex/values.yaml index 89d9c86f2..b9dc2d765 100644 --- a/helm/squidex/values.yaml +++ b/helm/squidex/values.yaml @@ -9,6 +9,9 @@ service: ## @param service.port Kubernetes Service port ## port: 80 + ## @param service.port Kubernetes Service port + ## + nodePort: null deployment: ## @param deployment.replicaCount Number of instances. ## From 3a30b63c2b6a2e19265785381b5d63f3dadd591a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 19 Aug 2024 11:47:02 +0200 Subject: [PATCH 3/4] Update GitHub Action Versions (#1118) Co-authored-by: github-actions[bot] --- .github/workflows/dev.yml | 14 +++++++------- .github/workflows/make-screenshots.yml | 8 ++++---- .github/workflows/release.yml | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index eae5dc053..99a1efd8f 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -22,10 +22,10 @@ jobs: uses: rlespinasse/github-slug-action@v4.5.0 - name: Prepare - Setup QEMU - uses: docker/setup-qemu-action@v3.1.0 + uses: docker/setup-qemu-action@v3.2.0 - name: Prepare - Setup Docker Buildx - uses: docker/setup-buildx-action@v3.4.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Prepare - Setup Node uses: actions/setup-node@v4.0.3 @@ -33,7 +33,7 @@ jobs: node-version: 18 - name: Build - BUILD - uses: docker/build-push-action@v6.4.1 + uses: docker/build-push-action@v6.7.0 with: load: true build-args: "SQUIDEX__RUNTIME__VERSION=7.0.0-dev-${{ env.BUILD_NUMBER }}" @@ -103,7 +103,7 @@ jobs: - name: Test - Upload Playwright Artifacts if: always() - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.6 with: name: playwright-report path: tools/e2e/playwright-report/ @@ -111,7 +111,7 @@ jobs: - name: Test - Upload Screenshots if: failure() - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.6 with: path: | tools/TestSuite/TestSuite.ApiTests/bin/Debug/net8.0/screenshots/ @@ -142,14 +142,14 @@ jobs: - name: Publish - Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Publish - Build & Push for Multi-Platforms if: github.event_name != 'pull_request' - uses: docker/build-push-action@v6.4.1 + uses: docker/build-push-action@v6.7.0 with: build-args: "SQUIDEX__RUNTIME__VERSION=7.0.0-dev-${{ env.BUILD_NUMBER }}" cache-from: type=gha diff --git a/.github/workflows/make-screenshots.yml b/.github/workflows/make-screenshots.yml index c0e8ccd64..215673196 100644 --- a/.github/workflows/make-screenshots.yml +++ b/.github/workflows/make-screenshots.yml @@ -12,10 +12,10 @@ jobs: uses: actions/checkout@v4.1.7 - name: Prepare - Setup QEMU - uses: docker/setup-qemu-action@v3.1.0 + uses: docker/setup-qemu-action@v3.2.0 - name: Prepare - Setup Docker Buildx - uses: docker/setup-buildx-action@v3.4.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Prepare - Setup Node uses: actions/setup-node@v4.0.3 @@ -23,7 +23,7 @@ jobs: node-version: 18 - name: Build - BUILD - uses: docker/build-push-action@v6.4.1 + uses: docker/build-push-action@v6.7.0 with: load: true cache-from: type=gha @@ -50,7 +50,7 @@ jobs: - name: Test - Upload Playwright Artifacts if: always() - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.6 with: name: snapshots path: tools/e2e/snapshots/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c914c9547..516c4a4a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,10 +17,10 @@ jobs: uses: rlespinasse/github-slug-action@v4.5.0 - name: Prepare - Setup QEMU - uses: docker/setup-qemu-action@v3.1.0 + uses: docker/setup-qemu-action@v3.2.0 - name: Prepare - Setup Docker Buildx - uses: docker/setup-buildx-action@v3.4.0 + uses: docker/setup-buildx-action@v3.6.1 - name: Prepare - Setup Node uses: actions/setup-node@v4.0.3 @@ -28,7 +28,7 @@ jobs: node-version: 18 - name: Build - BUILD - uses: docker/build-push-action@v6.4.1 + uses: docker/build-push-action@v6.7.0 with: load: true build-args: "SQUIDEX__BUILD__VERSION=${{ env.GITHUB_REF_SLUG }},SQUIDEX__RUNTIME__VERSION=${{ env.GITHUB_REF_SLUG }}" @@ -98,7 +98,7 @@ jobs: - name: Test - Upload Playwright Artifacts if: always() - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.3.6 with: name: playwright-report path: tools/e2e/playwright-report/ @@ -136,13 +136,13 @@ jobs: fi - name: Publish - Login to Docker Hub - uses: docker/login-action@v3.2.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Publish - Build & Push for Multi-Platforms - uses: docker/build-push-action@v6.4.1 + uses: docker/build-push-action@v6.7.0 with: build-args: "SQUIDEX__BUILD__VERSION=${{ env.GITHUB_REF_SLUG }},SQUIDEX__RUNTIME__VERSION=${{ env.GITHUB_REF_SLUG }}" cache-from: type=gha From 9733e8f63d3cf9c9eb435a3e83924dd675205d20 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 19 Aug 2024 14:47:52 +0200 Subject: [PATCH 4/4] Enrich required fields. (#1119) * Enrich required fields. * Remove unused properties. * Fix tests * Update tests --- .../Apps/Indexes/AppsIndex.cs | 12 +- .../Assets/AssetSlug.cs | 2 +- .../Contents/Commands/BulkUpdateContents.cs | 2 + .../Commands/EnrichContentDefaults.cs | 1 + .../Contents/Commands/UpsertContent.cs | 2 + .../Contents/ContentHeaders.cs | 12 ++ .../DomainObject/ContentDomainObject.cs | 6 +- .../Guards/ValidationExtensions.cs | 20 ++- .../Contents/Queries/ContentEnricher.cs | 9 +- .../Contents/Queries/Steps/ConvertData.cs | 2 +- .../Jobs/JobWorker.cs | 2 +- .../Schemas/Indexes/SchemasIndex.cs | 12 +- .../Contents/ContentsController.cs | 4 +- .../Contents/Models/BulkUpdateContentsDto.cs | 5 + .../Models/EnrichContentDefaultsDto.cs | 29 ++++ .../Assets/Queries/EnrichForCachingTests.cs | 1 + .../Contents/Queries/EnrichForCachingTests.cs | 1 + tools/.editorconfig | 3 + .../TestSuite.ApiTests/AssetFoldersTests.cs | 2 +- .../TestSuite.ApiTests/ContentQueryTests.cs | 2 +- .../TestSuite.ApiTests/ContentUpdateTests.cs | 155 ++++++++++++++++-- .../TestSuite.ApiTests/GraphQLTests.cs | 15 +- .../TestSuite.ApiTests.csproj | 20 +-- .../TestSuite.LoadTests.csproj | 8 +- .../TestSuite.Shared/ClientExtensions.cs | 2 +- .../TestSuite.Shared/ContentStrategies.cs | 49 ++++-- .../TestSuite.Shared/TestSuite.Shared.csproj | 14 +- 27 files changed, 300 insertions(+), 92 deletions(-) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs index a0074ef0d..733f1c931 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -227,11 +227,11 @@ public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable } // Do not use cancellation here as we already so far. - await appCache.AddAsync(new[] - { + await appCache.AddAsync( + [ new KeyValuePair(GetCacheKey(app.Id), app), new KeyValuePair(GetCacheKey(app.Name), app), - }, options.CacheDuration); + ], options.CacheDuration); return app; } @@ -244,10 +244,10 @@ public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable } // Do not use cancellation here as we already so far. - return appCache.RemoveAsync(new[] - { + return appCache.RemoveAsync( + [ GetCacheKey(id), GetCacheKey(name) - }); + ]); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs index 40d68a8e3..55355e8e2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs @@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Assets; public static class AssetSlug { - private static readonly HashSet Dot = new HashSet(new[] { '.' }); + private static readonly HashSet Dot = new HashSet(['.']); public static string ToAssetSlug(this string value) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs index 63d7febff..40d573aeb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateContents.cs @@ -29,5 +29,7 @@ public sealed class BulkUpdateContents : SquidexCommand, IAppCommand, ISchemaCom public bool OptimizeValidation { get; set; } + public bool EnrichRequiredFields { get; set; } + public BulkUpdateJob[]? Jobs { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/EnrichContentDefaults.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/EnrichContentDefaults.cs index 6a8d39dc9..62f52fcee 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/EnrichContentDefaults.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/EnrichContentDefaults.cs @@ -9,4 +9,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands; public sealed class EnrichContentDefaults : ContentCommand { + public bool EnrichRequiredFields { get; set; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs index 8207cbd48..0b8a5663e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs @@ -21,6 +21,8 @@ public sealed class UpsertContent : ContentDataCommand, ISchemaCommand public bool EnrichDefaults { get; set; } + public bool EnrichRequiredFields { get; set; } + public UpsertContent() { ContentId = DomainId.NewGuid(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs index da5e2c7a9..77f651d51 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs @@ -19,6 +19,7 @@ public static class ContentHeaders public const string KeyLanguages = "X-Languages"; public const string KeyNoCleanup = "X-NoCleanup"; public const string KeyNoEnrichment = "X-NoEnrichment"; + public const string KeyNoDefaults = "X-NoDefaults"; public const string KeyNoResolveLanguages = "X-NoResolveLanguages"; public const string KeyResolveFlow = "X-ResolveFlow"; public const string KeyResolveUrls = "X-ResolveUrls"; @@ -32,6 +33,7 @@ public static class ContentHeaders cache.AddHeader(KeyLanguages); cache.AddHeader(KeyNoCleanup); cache.AddHeader(KeyNoEnrichment); + cache.AddHeader(KeyNoDefaults); cache.AddHeader(KeyNoResolveLanguages); cache.AddHeader(KeyResolveFlow); cache.AddHeader(KeyResolveUrls); @@ -63,6 +65,16 @@ public static class ContentHeaders return builder.WithBoolean(KeyNoEnrichment, value); } + public static bool NoDefaults(this Context context) + { + return context.AsBoolean(KeyNoDefaults); + } + + public static ICloneBuilder WithNoDefaults(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(KeyNoDefaults, value); + } + public static bool Unpublished(this Context context) { return context.AsBoolean(KeyUnpublished); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index 13f2a38bc..2fa726d96 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -211,7 +211,7 @@ public partial class ContentDomainObject : DomainObject { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - var newData = operation.GenerateDefaultValues(Snapshot.EditingData.Clone()); + var newData = operation.GenerateDefaultValues(Snapshot.EditingData.Clone(), !c.EnrichRequiredFields); if (!newData.Equals(Snapshot.EditingData)) { @@ -263,7 +263,7 @@ public partial class ContentDomainObject : DomainObject c.Data = await operation.ExecuteCreateScriptAsync(c.Data, status, ct); } - c.Data = operation.GenerateDefaultValues(c.Data); + c.Data = operation.GenerateDefaultValues(c.Data, false); if (!c.DoNotValidate) { @@ -336,7 +336,7 @@ public partial class ContentDomainObject : DomainObject if (c.EnrichDefaults) { - newData = operation.GenerateDefaultValues(newData); + newData = operation.GenerateDefaultValues(newData, true); } if (newData.Equals(Snapshot.EditingData)) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs index 4ed82d679..24af0cbf9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs @@ -47,7 +47,8 @@ public static class ValidationExtensions operation.ThrowOnErrors(); } - public static async Task ValidateInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published, + public static async Task ValidateInputAsync(this ContentOperation operation, + ContentData data, bool optimize, bool published, CancellationToken ct) { var validator = GetValidator(operation, optimize, published); @@ -57,7 +58,8 @@ public static class ValidationExtensions operation.AddErrors(validator.Errors).ThrowOnErrors(); } - public static async Task ValidateInputPartialAsync(this ContentOperation operation, ContentData data, bool optimize, bool published, + public static async Task ValidateInputPartialAsync(this ContentOperation operation, + ContentData data, bool optimize, bool published, CancellationToken ct) { var validator = GetValidator(operation, optimize, published); @@ -67,7 +69,8 @@ public static class ValidationExtensions operation.AddErrors(validator.Errors).ThrowOnErrors(); } - public static async Task ValidateContentAsync(this ContentOperation operation, ContentData data, bool optimize, bool published, + public static async Task ValidateContentAsync(this ContentOperation operation, + ContentData data, bool optimize, bool published, CancellationToken ct) { var validator = GetValidator(operation, optimize, published); @@ -77,7 +80,8 @@ public static class ValidationExtensions operation.AddErrors(validator.Errors).ThrowOnErrors(); } - public static async Task ValidateContentAndInputAsync(this ContentOperation operation, ContentData data, bool optimize, bool published, + public static async Task ValidateContentAndInputAsync(this ContentOperation operation, + ContentData data, bool optimize, bool published, CancellationToken ct) { var validator = GetValidator(operation, optimize, published); @@ -87,18 +91,20 @@ public static class ValidationExtensions operation.AddErrors(validator.Errors).ThrowOnErrors(); } - public static ContentData GenerateDefaultValues(this ContentOperation operation, ContentData data) + public static ContentData GenerateDefaultValues(this ContentOperation operation, + ContentData data, bool ignoreRequired) { var converter = new ContentConverter( operation.Components, operation.Schema); - converter.Add(new AddDefaultValues(operation.Partition()) { IgnoreRequiredFields = true }); + converter.Add(new AddDefaultValues(operation.Partition()) { IgnoreRequiredFields = ignoreRequired }); return converter.Convert(data); } - public static ContentData InvokeUpdates(this ContentOperation operation, ContentData data, ContentData currentData, bool canUnset) + public static ContentData InvokeUpdates(this ContentOperation operation, ContentData data, + ContentData currentData, bool canUnset) { var converter = new ContentConverter( diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 869835273..a00d3e201 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -67,6 +67,7 @@ public sealed class ContentEnricher : IContentEnricher { var result = SimpleMapper.Map(content, new EnrichedContent()); + // Clone the data to keep the existing value intact (for example when cached in memory). if (cloneData) { using (Telemetry.Activities.StartActivity("ContentEnricher/CloneData")) @@ -86,12 +87,8 @@ public sealed class ContentEnricher : IContentEnricher { return schemaCache.GetOrAdd(id, async x => { - var schema = await appProvider.GetSchemaAsync(context.App.Id, x, false, ct); - - if (schema == null) - { - throw new DomainObjectNotFoundException(x.ToString()); - } + var schema = await appProvider.GetSchemaAsync(context.App.Id, x, false, ct) + ?? throw new DomainObjectNotFoundException(x.ToString()); var components = await appProvider.GetComponentsAsync(schema, ct); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs index 0854e45eb..a51a6fff6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs @@ -133,7 +133,7 @@ public sealed class ConvertData : IContentEnricherStep converter.Add(new ResolveFromPreviousPartitioning(context.App.Languages)); - if (!context.IsFrontendClient) + if (!context.IsFrontendClient && !context.NoDefaults()) { converter.Add(new AddDefaultValues(context.App.PartitionResolver()) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs index 41c154ad8..9b8a2664c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Jobs/JobWorker.cs @@ -27,7 +27,7 @@ public sealed class JobWorker : processorFactory = key => { - return (JobProcessor)objectFactory(serviceProvider, new object[] { key }); + return (JobProcessor)objectFactory(serviceProvider, [key]); }; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs index 376209987..bcfe51c77 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -218,11 +218,11 @@ public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex } // Do not use cancellation here as we already so far. - await schemaCache.AddAsync(new[] - { + await schemaCache.AddAsync( + [ new KeyValuePair(GetCacheKey(schema.AppId.Id, schema.Id), schema), new KeyValuePair(GetCacheKey(schema.AppId.Id, schema.Name), schema), - }, options.CacheDuration); + ], options.CacheDuration); return schema; } @@ -235,10 +235,10 @@ public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex } // Do not use cancellation here as we already so far. - return schemaCache.RemoveAsync(new[] - { + return schemaCache.RemoveAsync( + [ GetCacheKey(appId, id), GetCacheKey(appId, name) - }); + ]); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 1f584281f..1612e9a41 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -464,9 +464,9 @@ public sealed class ContentsController : ApiController [AcceptHeader.Unpublished] [AcceptHeader.Languages] [ApiCosts(1)] - public async Task PutContentDefaults(string app, string schema, DomainId id) + public async Task PutContentDefaults(string app, string schema, DomainId id, EnrichContentDefaultsDto request) { - var command = new EnrichContentDefaults { ContentId = id }; + var command = request.ToCommand(id); var response = await InvokeCommandAsync(command); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs index b3506b598..6cf20c48d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsDto.cs @@ -33,6 +33,11 @@ public sealed class BulkUpdateContentsDto /// public bool DoNotScript { get; set; } = true; + /// + /// True, to also enrich required fields. Default: false. + /// + public bool EnrichRequiredFields { get; set; } + /// /// True to turn off validation for faster inserts. Default: false. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs new file mode 100644 index 000000000..cb72f5caf --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/EnrichContentDefaultsDto.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public class EnrichContentDefaultsDto +{ + /// + /// True, to also enrich required fields. Default: false. + /// + [FromQuery(Name = "enrichRequiredFields")] + public bool EnrichRequiredFields { get; set; } + + public EnrichContentDefaults ToCommand(DomainId id) + { + var command = SimpleMapper.Map(this, new EnrichContentDefaults { ContentId = id }); + + return command; + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/EnrichForCachingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/EnrichForCachingTests.cs index be809e6e0..7162ec246 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/EnrichForCachingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/EnrichForCachingTests.cs @@ -38,6 +38,7 @@ public class EnrichForCachingTests : GivenContext "X-Languages", "X-NoCleanup", "X-NoEnrichment", + "X-NoDefaults", "X-NoResolveLanguages", "X-ResolveFlow", "X-ResolveUrls", diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs index fda5c4105..5387119f9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs @@ -39,6 +39,7 @@ public class EnrichForCachingTests : GivenContext "X-Languages", "X-NoCleanup", "X-NoEnrichment", + "X-NoDefaults", "X-NoResolveLanguages", "X-ResolveFlow", "X-ResolveUrls", diff --git a/tools/.editorconfig b/tools/.editorconfig index facac6742..d91ff0eeb 100644 --- a/tools/.editorconfig +++ b/tools/.editorconfig @@ -30,6 +30,9 @@ dotnet_diagnostic.IDE0305.severity = none # CA1707: Identifiers should not contain underscores dotnet_diagnostic.CA1707.severity = none +# CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = none + # CA2016: Forward the 'CancellationToken' parameter to methods dotnet_diagnostic.CA2016.severity = none diff --git a/tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs b/tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs index b52bca1f9..7149b3ea0 100644 --- a/tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs @@ -145,7 +145,7 @@ public class AssetFoldersTests : IClassFixture // STEP 3: Query by nested id. var folders2 = await _.Client.Assets.GetAssetFoldersAsync(folder1.Id); - Assert.Equal(new[] { folder2.Id }, folders2.Items.Select(x => x.Id)); + Assert.Equal([folder2.Id], folders2.Items.Select(x => x.Id)); // STEP 3: Query all diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 8ba6b4bb2..a32305e31 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -633,7 +633,7 @@ public class ContentQueryTests : IClassFixture } }; - var results = await _.Client.SharedDynamicContents.GraphQlAsync(new[] { query1, query2 }); + var results = await _.Client.SharedDynamicContents.GraphQlAsync([query1, query2]); var items1 = results.ElementAt(0).Data.Items; var items2 = results.ElementAt(1).Data.Items; diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index 8260372ea..dfc2efd62 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -941,7 +941,7 @@ public class ContentUpdateTests : IClassFixture [InlineData(ContentStrategies.EnrichDefaults.Bulk)] [InlineData(ContentStrategies.EnrichDefaults.BulkWithSchema)] [InlineData(ContentStrategies.EnrichDefaults.BulkShared)] - public async Task Should_enrich_defaults(ContentStrategies.EnrichDefaults strategy) + public async Task Should_enrich_default_fields(ContentStrategies.EnrichDefaults strategy) { var schemaName = $"schema-{Guid.NewGuid()}"; @@ -949,14 +949,6 @@ public class ContentUpdateTests : IClassFixture var schemaRequest = new CreateSchemaDto { Name = schemaName, - Fields = - [ - new UpsertSchemaFieldDto - { - Name = TestEntityData.NumberField, - Properties = new NumberFieldPropertiesDto() - }, - ], IsPublished = true }; @@ -968,30 +960,161 @@ public class ContentUpdateTests : IClassFixture // STEP 1: Create a new item. var content_0 = await contents.CreateAsync([], ContentCreateOptions.AsPublish); - Assert.Null(content_0.Data.GetValueOrDefault(TestEntityData.StringField)); + Assert.Null(content_0.Data.GetValueOrDefault("fieldDefault")); - // STEP 2: Add required field. + // STEP 2: Add new fields. var fieldRequest = new AddFieldDto { - Name = TestEntityData.StringField, + Name = "fieldDefault", + Properties = new StringFieldPropertiesDto + { + DefaultValue = "Hello Squidex", + IsRequired = false + } + }; + + await _.Client.Schemas.PostFieldAsync(schemaName, fieldRequest); + + + // STEP 3: Create required field. + await _.Client.EnrichDefaultsAsync(content_0, content_0.Data, strategy, false); + + + // STEP 4: Get content. + var context = QueryContext.Default.WithHeaderHandler(request => + { + request.Headers.TryAddWithoutValidation("X-NoDefaults", "1"); + }); + + var content_1 = await contents.GetAsync(content_0.Id, context); + + Assert.Equal("Hello Squidex", content_1.Data["fieldDefault"]!["iv"]!.ToString()); + } + + [Theory] + [InlineData(ContentStrategies.EnrichDefaults.Normal)] + [InlineData(ContentStrategies.EnrichDefaults.Bulk)] + [InlineData(ContentStrategies.EnrichDefaults.BulkWithSchema)] + [InlineData(ContentStrategies.EnrichDefaults.BulkShared)] + public async Task Should_enrich_non_required_default_fields(ContentStrategies.EnrichDefaults strategy) + { + var schemaName = $"schema-{Guid.NewGuid()}"; + + // STEP 0: Create initial schema. + var schemaRequest = new CreateSchemaDto + { + Name = schemaName, + IsPublished = true + }; + + await _.Client.Schemas.PostSchemaAsync(schemaRequest); + + var contents = _.Client.DynamicContents(schemaName); + + + // STEP 1: Create a new item. + var content_0 = await contents.CreateAsync([], ContentCreateOptions.AsPublish); + + Assert.Null(content_0.Data.GetValueOrDefault("fieldDefault")); + Assert.Null(content_0.Data.GetValueOrDefault("fieldRequired")); + + + // STEP 2: Add new fields. + var fieldRequest1 = new AddFieldDto + { + Name = "fieldDefault", + Properties = new StringFieldPropertiesDto + { + DefaultValue = "Hello Squidex", + IsRequired = false + } + }; + + var fieldRequest2 = new AddFieldDto + { + Name = "fieldRequired", Properties = new StringFieldPropertiesDto { - DefaultValue = "Hello Squidex" + DefaultValue = "Hello Required", + IsRequired = true } }; + await _.Client.Schemas.PostFieldAsync(schemaName, fieldRequest1); + await _.Client.Schemas.PostFieldAsync(schemaName, fieldRequest2); + + + // STEP 3: Create required field. + await _.Client.EnrichDefaultsAsync(content_0, content_0.Data, strategy, false); + + + // STEP 4: Get content. + var context = QueryContext.Default.WithHeaderHandler(request => + { + request.Headers.TryAddWithoutValidation("X-NoDefaults", "1"); + }); + + var content_1 = await contents.GetAsync(content_0.Id, context); + + Assert.Equal("Hello Squidex", content_1.Data["fieldDefault"]!["iv"]!.ToString()); + Assert.Null(content_0.Data.GetValueOrDefault("fieldRequired")); + } + + [Theory] + [InlineData(ContentStrategies.EnrichDefaults.Normal)] + [InlineData(ContentStrategies.EnrichDefaults.Bulk)] + [InlineData(ContentStrategies.EnrichDefaults.BulkWithSchema)] + [InlineData(ContentStrategies.EnrichDefaults.BulkShared)] + public async Task Should_enrich_required_default_field_if_flag_is_true(ContentStrategies.EnrichDefaults strategy) + { + var schemaName = $"schema-{Guid.NewGuid()}"; + + // STEP 0: Create initial schema. + var schemaRequest = new CreateSchemaDto + { + Name = schemaName, + IsPublished = true + }; + + await _.Client.Schemas.PostSchemaAsync(schemaRequest); + + var contents = _.Client.DynamicContents(schemaName); + + + // STEP 1: Create a new item. + var content_0 = await contents.CreateAsync([], ContentCreateOptions.AsPublish); + + Assert.Null(content_0.Data.GetValueOrDefault("fieldDefault")); + Assert.Null(content_0.Data.GetValueOrDefault("fieldRequired")); + + + // STEP 2: Add new field. + var fieldRequest = new AddFieldDto + { + Name = "fieldRequired", + Properties = new StringFieldPropertiesDto + { + DefaultValue = "Hello Required", + IsRequired = true + } + }; await _.Client.Schemas.PostFieldAsync(schemaName, fieldRequest); // STEP 3: Create required field. - await _.Client.EnrichDefaultsAsync(content_0, content_0.Data, strategy); + await _.Client.EnrichDefaultsAsync(content_0, content_0.Data, strategy, true); // STEP 4: Get content. - var content_1 = await contents.GetAsync(content_0.Id); + var context = QueryContext.Default.WithHeaderHandler(request => + { + request.Headers.TryAddWithoutValidation("X-NoDefaults", "1"); + }); + + var content_1 = await contents.GetAsync(content_0.Id, context); - Assert.Equal("Hello Squidex", content_1.Data[TestEntityData.StringField]!["iv"]!.ToString()); + Assert.Equal("Hello Required", content_1.Data["fieldRequired"]!["iv"]!.ToString()); } [Fact] diff --git a/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs b/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs index 78b0efda4..2a496285e 100644 --- a/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs @@ -239,7 +239,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x.Data.Name) .Order(); - Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames); + Assert.Equal(["Leipzig", "Munich"], cityNames); } [Fact] @@ -276,7 +276,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data"]!["name"]!.Value()) .Order(); - Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames); + Assert.Equal(["Leipzig", "Munich"], cityNames); } [Fact] @@ -313,7 +313,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data"]!["name"]!.Value()) .Order(); - Assert.Equal(new[] { "Leipzig" }, cityNames); + Assert.Equal(["Leipzig"], cityNames); } [Fact] @@ -341,7 +341,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data"]!["name"]!.Value()) .Order(); - Assert.Equal(new[] { "Bavaria", "Saxony" }, stateNames); + Assert.Equal(["Bavaria", "Saxony"], stateNames); } [Fact] @@ -369,7 +369,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x!["data"]!["name"]!.Value()) .Order(); - Assert.Equal(new[] { "Saxony" }, stateNames); + Assert.Equal(["Saxony"], stateNames); } [Fact] @@ -392,7 +392,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data__dynamic"]!["name"]!["iv"]!.Value()) .Order(); - Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames); + Assert.Equal(["Leipzig", "Munich"], cityNames); } [Fact] @@ -458,7 +458,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data"]!["name"]!.Value()) .Order(); - Assert.Equal(new[] { "Bavaria", "Leipzig", "Munich", "Saxony" }, names); + Assert.Equal(["Bavaria", "Leipzig", "Munich", "Saxony"], names); } [Fact] @@ -523,6 +523,7 @@ public sealed class GraphQLTests : IClassFixture "X-Flatten", "X-Languages", "X-NoCleanup", + "X-NoDefaults", "X-NoEnrichment", "X-NoResolveLanguages", "X-ResolveFlow", diff --git a/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj index 64dea9f8d..b40cea3fd 100644 --- a/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj +++ b/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj @@ -8,20 +8,20 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj b/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj index e54910295..3c8d60379 100644 --- a/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj +++ b/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj @@ -7,14 +7,14 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers diff --git a/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs b/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs index 78d493956..e8d5cfd82 100644 --- a/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs +++ b/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs @@ -338,7 +338,7 @@ public static class ClientExtensions return temp; } - public static async Task UploadInChunksAsync(this IAssetsClient client, ProgressHandler progress, FileParameter fileParameter, string? id = null) + public static async Task UploadInChunksAsync(this IAssetsClient client, ProgressHandler progress, FileParameter fileParameter, string? id = null) { var pausingStream = new PauseStream(fileParameter.Data, 0.25); var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType) diff --git a/tools/TestSuite/TestSuite.Shared/ContentStrategies.cs b/tools/TestSuite/TestSuite.Shared/ContentStrategies.cs index 6b3a2503e..f2dce7af6 100644 --- a/tools/TestSuite/TestSuite.Shared/ContentStrategies.cs +++ b/tools/TestSuite/TestSuite.Shared/ContentStrategies.cs @@ -21,7 +21,8 @@ public static partial class ContentStrategies BulkPermanent } - public static Task DeleteAsync(this ISquidexClient client, ContentBase content, Deletion strategy) + public static Task DeleteAsync(this ISquidexClient client, ContentBase content, + Deletion strategy) { IContentsClient GetClient() { @@ -75,7 +76,8 @@ public static partial class ContentStrategies BulkShared } - public static Task UpdateAsync(this ISquidexClient client, ContentBase content, object data, Update strategy) + public static Task UpdateAsync(this ISquidexClient client, ContentBase content, object data, + Update strategy) { IContentsClient GetClient() { @@ -157,7 +159,8 @@ public static partial class ContentStrategies BulkShared } - public static Task PatchAsync(this ISquidexClient client, ContentBase content, object data, Patch strategy) + public static Task PatchAsync(this ISquidexClient client, ContentBase content, object data, + Patch strategy) { IContentsClient GetClient() { @@ -242,7 +245,8 @@ public static partial class ContentStrategies UpdateBulk } - public static Task EnrichDefaultsAsync(this ISquidexClient client, ContentBase content, object data, EnrichDefaults strategy) + public static Task EnrichDefaultsAsync(this ISquidexClient client, ContentBase content, object data, + EnrichDefaults strategy, bool requiredFields) { IContentsClient GetClient() { @@ -252,11 +256,27 @@ public static partial class ContentStrategies switch (strategy) { case EnrichDefaults.Normal: - return GetClient().EnrichDefaultsAsync(content.Id); + var createOptions = new ContentEnrichDefaultsOptions + { + EnrichRequiredFields = requiredFields + }; + + return GetClient().EnrichDefaultsAsync(content.Id, createOptions); case EnrichDefaults.Update: - return GetClient().UpdateAsync(content.Id, data, ContentUpdateOptions.AsEnrichDefaults); + var updateOptions = new ContentUpdateOptions + { + EnrichDefaults = true + }; + + return GetClient().UpdateAsync(content.Id, data, updateOptions); case EnrichDefaults.Upsert: - return GetClient().UpsertAsync(content.Id, data, ContentUpsertOptions.AsEnrichDefaults); + var upsertOptions = new ContentUpsertOptions + { + EnrichDefaults = true, + EnrichRequiredFields = requiredFields + }; + + return GetClient().UpsertAsync(content.Id, data, upsertOptions); case EnrichDefaults.Bulk: return GetClient().BulkUpdateAsync(new BulkUpdate { @@ -267,7 +287,8 @@ public static partial class ContentStrategies Type = BulkUpdateType.EnrichDefaults, Id = content.Id }, - ] + ], + EnrichRequiredFields = requiredFields }); case EnrichDefaults.UpdateBulk: return GetClient().BulkUpdateAsync(new BulkUpdate @@ -281,7 +302,8 @@ public static partial class ContentStrategies Data = data, EnrichDefaults = true, }, - ] + ], + EnrichRequiredFields = requiredFields }); case EnrichDefaults.UpsertBulk: return GetClient().BulkUpdateAsync(new BulkUpdate @@ -295,7 +317,8 @@ public static partial class ContentStrategies Data = data, EnrichDefaults = true, }, - ] + ], + EnrichRequiredFields = requiredFields }); case EnrichDefaults.BulkWithSchema: return GetClient().BulkUpdateAsync(new BulkUpdate @@ -308,7 +331,8 @@ public static partial class ContentStrategies Id = content.Id, Schema = content.SchemaName }, - ] + ], + EnrichRequiredFields = requiredFields }); case EnrichDefaults.BulkShared: return GetSharedClient(client).BulkUpdateAsync(new BulkUpdate @@ -321,7 +345,8 @@ public static partial class ContentStrategies Id = content.Id, Schema = content.SchemaName }, - ] + ], + EnrichRequiredFields = requiredFields }); default: return Task.CompletedTask; diff --git a/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index bed51df97..642b4d6a1 100644 --- a/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -7,22 +7,22 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + - - + +