Browse Source

Fix timeouts.

pull/933/head
Sebastian 4 years ago
parent
commit
16da556954
  1. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  2. 70
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  3. 9
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs
  4. 80
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  5. 82
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs
  6. 30
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  7. 9
      backend/src/Squidex.Infrastructure/Diagnostics/Diagnoser.cs
  8. 32
      backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs
  9. 4
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  10. 86
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  11. 148
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs
  12. 22
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs

10
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -364,12 +364,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var deserialized = serializer.Deserialize<object>(job, actionHandler.DataType);
using (var cts = new CancellationTokenSource(GetTimeoutInMs()))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct))
{
result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token);
}
// Enforce a timeout after a configured time span.
combined.CancelAfter(GetTimeoutInMs());
result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token);
}
}
catch (Exception ex)

70
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -46,25 +46,25 @@ namespace Squidex.Domain.Apps.Core.Scripting
Guard.NotNull(vars);
Guard.NotNullOrEmpty(script);
using (var cts = new CancellationTokenSource(timeoutExecution))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct))
{
var context =
CreateEngine<JsonValue?>(options, combined.Token)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions);
// Enforce a timeout after a configured time span.
combined.CancelAfter(timeoutExecution);
context.Engine.SetValue("complete", new Action<JsValue?>(value =>
{
context.Complete(JsonMapper.Map(value));
}));
var context =
CreateEngine<JsonValue?>(options, combined.Token)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions);
context.Engine.SetValue("complete", new Action<JsValue?>(value =>
{
context.Complete(JsonMapper.Map(value));
}));
var result = Execute(context.Engine, script);
var result = Execute(context.Engine, script);
return await context.CompleteAsync() ?? JsonMapper.Map(result);
}
return await context.CompleteAsync() ?? JsonMapper.Map(result);
}
}
@ -74,38 +74,38 @@ namespace Squidex.Domain.Apps.Core.Scripting
Guard.NotNull(vars);
Guard.NotNullOrEmpty(script);
using (var cts = new CancellationTokenSource(timeoutExecution))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct))
{
var context =
// Enforce a timeout after a configured time span.
combined.CancelAfter(timeoutExecution);
var context =
CreateEngine<ContentData>(options, combined.Token)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions);
context.Engine.SetValue("complete", new Action<JsValue?>(_ =>
{
context.Complete(vars.Data!);
}));
context.Engine.SetValue("complete", new Action<JsValue?>(_ =>
{
context.Complete(vars.Data!);
}));
context.Engine.SetValue("replace", new Action(() =>
{
var dataInstance = context.Engine.GetValue("ctx").AsObject().Get("data");
context.Engine.SetValue("replace", new Action(() =>
{
var dataInstance = context.Engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
if (!context.IsCompleted && data.TryUpdate(out var modified))
{
if (!context.IsCompleted && data.TryUpdate(out var modified))
{
context.Complete(modified);
}
context.Complete(modified);
}
}));
}
}));
Execute(context.Engine, script);
Execute(context.Engine, script);
return await context.CompleteAsync() ?? vars.Data!;
}
return await context.CompleteAsync() ?? vars.Data!;
}
}

9
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
BulkUpdateJob CommandJob,
BulkUpdateAssets Command,
ConcurrentBag<BulkUpdateResultItem> Results,
CancellationToken CancellationToken);
CancellationToken Aborted);
public AssetsBulkUpdateCommandMiddleware(IContextProvider contextProvider, ILogger<AssetsBulkUpdateCommandMiddleware> log)
{
@ -56,6 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2)
};
// Each job can create exactly one command.
var createCommandsBlock = new TransformBlock<BulkTask, BulkTaskCommand?>(task =>
{
try
@ -69,6 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
}
}, executionOptions);
// Execute the commands in batches
var executeCommandBlock = new ActionBlock<BulkTaskCommand?>(async command =>
{
try
@ -113,6 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
createCommandsBlock.Complete();
// Wait for all commands to be executed.
await executeCommandBlock.Completion;
context.Complete(new BulkUpdateResult(results));
@ -150,14 +153,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject
private BulkTaskCommand? CreateCommand(BulkTask task)
{
var id = task.CommandJob.Id;
try
{
var command = CreateCommandCore(task);
// Set the asset id here in case we have another way to resolve ids.
command.AssetId = id;
return new BulkTaskCommand(task, id, command, task.CancellationToken);
return new BulkTaskCommand(task, id, command, task.Aborted);
}
catch (Exception ex)
{

80
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -218,96 +218,96 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(Context context, DomainId parentId,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetFolderRepository.QueryAsync(context.App.Id, parentId, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutQuery);
return await assetFolderRepository.QueryAsync(context.App.Id, parentId, combined.Token);
}
}
private async Task<IResultList<IAssetFolderEntity>> QueryFoldersCoreAsync(DomainId appId, DomainId parentId,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetFolderRepository.QueryAsync(appId, parentId, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutQuery);
return await assetFolderRepository.QueryAsync(appId, parentId, combined.Token);
}
}
private async Task<IResultList<IAssetEntity>> QueryCoreAsync(Context context, DomainId? parentId, Q q,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.QueryAsync(context.App.Id, parentId, q, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutQuery);
return await assetRepository.QueryAsync(context.App.Id, parentId, q, combined.Token);
}
}
private async Task<IAssetFolderEntity?> FindFolderCoreAsync(DomainId appId, DomainId id,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetFolderRepository.FindAssetFolderAsync(appId, id, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind);
return await assetFolderRepository.FindAssetFolderAsync(appId, id, combined.Token);
}
}
private async Task<IAssetEntity?> FindByHashCoreAsync(Context context, string hash, string fileName, long fileSize,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind);
return await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, combined.Token);
}
}
private async Task<IAssetEntity?> FindBySlugCoreAsync(Context context, string slug,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind);
return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, combined.Token);
}
}
private async Task<IAssetEntity?> FindCoreAsync(DomainId id,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetAsync(id, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind);
return await assetRepository.FindAssetAsync(id, combined.Token);
}
}
private async Task<IAssetEntity?> FindCoreAsync(Context context, DomainId id,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await assetRepository.FindAssetAsync(context.App.Id, id, combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind);
return await assetRepository.FindAssetAsync(context.App.Id, id, combined.Token);
}
}
}

82
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs

@ -34,12 +34,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
private sealed record BulkTask(
ICommandBus Bus,
string Schema,
NamedId<DomainId> SchemaId,
int JobIndex,
BulkUpdateJob CommandJob,
BulkUpdateContents Command,
ConcurrentBag<BulkUpdateResultItem> Results,
CancellationToken CancellationToken);
CancellationToken Aborted);
public ContentsBulkUpdateCommandMiddleware(
IContentQueryService contentQuery,
@ -64,6 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2)
};
// Each job can create one or more commands.
var createCommandsBlock = new TransformManyBlock<BulkTask, BulkTaskCommand>(async task =>
{
try
@ -77,6 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
}
}, executionOptions);
// Execute the commands in batches.
var executeCommandBlock = new ActionBlock<BulkTaskCommand>(async command =>
{
try
@ -98,15 +100,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
.WithUnpublished(true)
.WithoutTotal());
var requestedSchema = bulkUpdates.SchemaId.Name;
var results = new ConcurrentBag<BulkUpdateResultItem>();
for (var i = 0; i < bulkUpdates.Jobs.Length; i++)
{
var task = new BulkTask(
context.CommandBus,
requestedSchema,
bulkUpdates.SchemaId,
i,
bulkUpdates.Jobs[i],
bulkUpdates,
@ -121,6 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
createCommandsBlock.Complete();
// Wait for all commands to be executed.
await executeCommandBlock.Completion;
context.Complete(new BulkUpdateResult(results));
@ -158,11 +159,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
private async Task<IEnumerable<BulkTaskCommand>> CreateCommandsAsync(BulkTask task)
{
// The task parallel pipeline does not allow async-enumerable.
var commands = new List<BulkTaskCommand>();
try
{
var resolvedIds = await FindIdAsync(task);
// Check whether another schema is defined for the current job and override the schema id if necessary.
var overridenSchema = task.CommandJob.Schema;
if (!string.IsNullOrWhiteSpace(overridenSchema))
{
var schema = await contentQuery.GetSchemaOrThrowAsync(contextProvider.Context, overridenSchema, task.Aborted);
// Task is immutable, so we have to create a copy.
task = task with { SchemaId = schema.NamedId() };
}
// The bulk command can be invoke in a schema controller or without a schema controller, therefore the name might be null.
if (task.SchemaId == null)
{
throw new DomainObjectNotFoundException("undefined");
}
var resolvedIds = await FindIdAsync(task, task.SchemaId.Name);
if (resolvedIds.Length == 0)
{
@ -173,15 +191,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
try
{
var command = await CreateCommandAsync(task);
var command = CreateCommand(task);
command.ContentId = id;
commands.Add(new BulkTaskCommand(task, id, command, task.CancellationToken));
commands.Add(new BulkTaskCommand(task, id, command, task.Aborted));
}
catch (Exception ex)
{
log.LogError(ex, "Failed to execute content bulk job with index {index} of type {type}.",
log.LogError(ex, "Failed to create content bulk job with index {index} of type {type}.",
task.JobIndex,
task.CommandJob.Type);
@ -191,13 +208,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
}
catch (Exception ex)
{
task.Results.Add(new BulkUpdateResultItem(null, task.JobIndex, ex));
task.Results.Add(new BulkUpdateResultItem(task.CommandJob.Id, task.JobIndex, ex));
}
return commands;
}
private async Task<ContentCommand> CreateCommandAsync(BulkTask task)
private ContentCommand CreateCommand(BulkTask task)
{
var job = task.CommandJob;
@ -207,7 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new CreateContent();
await EnrichAndCheckPermissionAsync(task, command, PermissionIds.AppContentsCreate);
EnrichAndCheckPermission(task, command, PermissionIds.AppContentsCreate);
return command;
}
@ -215,7 +232,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new UpdateContent();
await EnrichAndCheckPermissionAsync(task, command, PermissionIds.AppContentsUpdateOwn);
EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpdateOwn);
return command;
}
@ -223,7 +240,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new UpsertContent();
await EnrichAndCheckPermissionAsync(task, command, PermissionIds.AppContentsUpsert);
EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpsert);
return command;
}
@ -231,7 +248,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new PatchContent();
await EnrichAndCheckPermissionAsync(task, command, PermissionIds.AppContentsUpdateOwn);
EnrichAndCheckPermission(task, command, PermissionIds.AppContentsUpdateOwn);
return command;
}
@ -239,7 +256,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new ValidateContent();
await EnrichAndCheckPermissionAsync(task, command, PermissionIds.AppContentsReadOwn);
EnrichAndCheckPermission(task, command, PermissionIds.AppContentsReadOwn);
return command;
}
@ -247,7 +264,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new ChangeContentStatus { Status = job.Status ?? Status.Draft };
await EnrichAndCheckPermissionAsync(task, command, PermissionIds.AppContentsChangeStatusOwn);
EnrichAndCheckPermission(task, command, PermissionIds.AppContentsChangeStatusOwn);
return command;
}
@ -255,7 +272,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
var command = new DeleteContent();
await EnrichAndCheckPermissionAsync(task, command, PermissionIds.AppContentsDeleteOwn);
EnrichAndCheckPermission(task, command, PermissionIds.AppContentsDeleteOwn);
return command;
}
@ -265,29 +282,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
}
}
private async Task EnrichAndCheckPermissionAsync<T>(BulkTask task, T command, string permissionId) where T : ContentCommand
private void EnrichAndCheckPermission<T>(BulkTask task, T command, string permissionId) where T : ContentCommand
{
SimpleMapper.Map(task.Command, command);
SimpleMapper.Map(task.CommandJob, command);
var overridenSchema = task.CommandJob.Schema;
if (!string.IsNullOrWhiteSpace(overridenSchema))
{
var schema = await contentQuery.GetSchemaOrThrowAsync(contextProvider.Context, overridenSchema, task.CancellationToken);
command.SchemaId = schema.NamedId();
}
if (!contextProvider.Context.Allows(permissionId, command.SchemaId.Name))
{
throw new DomainForbiddenException("Forbidden");
}
command.SchemaId = task.SchemaId;
command.ExpectedVersion = task.Command.ExpectedVersion;
}
private async Task<DomainId[]> FindIdAsync(BulkTask task)
private async Task<DomainId[]> FindIdAsync(BulkTask task, string schema)
{
var id = task.CommandJob.Id;
@ -300,19 +309,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
{
task.CommandJob.Query.Take = task.CommandJob.ExpectedCount;
var existing = await contentQuery.QueryAsync(contextProvider.Context, task.Schema, Q.Empty.WithJsonQuery(task.CommandJob.Query), task.CancellationToken);
var existingQuery = Q.Empty.WithJsonQuery(task.CommandJob.Query);
var existingResult = await contentQuery.QueryAsync(contextProvider.Context, schema, existingQuery, task.Aborted);
if (existing.Total > task.CommandJob.ExpectedCount)
if (existingResult.Total > task.CommandJob.ExpectedCount)
{
throw new DomainException(T.Get("contents.bulkInsertQueryNotUnique"));
}
if (existing.Count == 0 && task.CommandJob.Type == BulkUpdateContentType.Upsert)
// Upsert means that we either update the content if we find it or that we create a new one.
// Therefore we create a new ID if we cannot find the ID for the query.
if (existingResult.Count == 0 && task.CommandJob.Type == BulkUpdateContentType.Upsert)
{
return new[] { DomainId.NewGuid() };
}
return existing.Select(x => x.Id).ToArray();
return existingResult.Select(x => x.Id).ToArray();
}
if (task.CommandJob.Type is BulkUpdateContentType.Create or BulkUpdateContentType.Upsert)

30
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -218,36 +218,36 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, ISchemaEntity schema,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), ct);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutQuery);
return await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), combined.Token);
}
}
private async Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, Q q, List<ISchemaEntity> schemas,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutQuery))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), ct);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutQuery);
return await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), combined.Token);
}
}
private async Task<IContentEntity?> FindCoreAsync(Context context, DomainId id, ISchemaEntity schema,
CancellationToken ct)
{
using (var timeout = new CancellationTokenSource(options.TimeoutFind))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct))
{
return await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), combined.Token);
}
// Enforce a hard timeout
combined.CancelAfter(options.TimeoutFind);
return await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), combined.Token);
}
}

9
backend/src/Squidex.Infrastructure/Diagnostics/Diagnoser.cs

@ -108,7 +108,10 @@ namespace Squidex.Infrastructure.Diagnostics
}
using var cts = new CancellationTokenSource(DefaultTimeout);
using var ctl = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct);
using var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct);
// Enforce a hard timeout.
combined.CancelAfter(DefaultTimeout);
var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
@ -121,7 +124,7 @@ namespace Squidex.Infrastructure.Diagnostics
process.StartInfo.UseShellExecute = false;
process.Start();
await process.WaitForExitAsync(ctl.Token);
await process.WaitForExitAsync(combined.Token);
if (process.ExitCode != 0)
{
@ -132,7 +135,7 @@ namespace Squidex.Infrastructure.Diagnostics
{
var name = $"diagnostics/{extension}/{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}.{extension}";
await assetStore.UploadAsync(name, fs, true, ctl.Token);
await assetStore.UploadAsync(name, fs, true, combined.Token);
}
}
finally

32
backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs

@ -33,29 +33,29 @@ namespace Squidex.Infrastructure.Email
var smtpClient = clientPool.Get();
try
{
using (var timeout = new CancellationTokenSource(options.Timeout))
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
using (var combined = CancellationTokenSource.CreateLinkedTokenSource(ct, timeout.Token))
{
await EnsureConnectedAsync(smtpClient, combined.Token);
// Enforce a hard timeout from the configuration.
combined.CancelAfter(options.Timeout);
await EnsureConnectedAsync(smtpClient, combined.Token);
var smtpMessage = new MimeMessage();
var smtpMessage = new MimeMessage();
smtpMessage.From.Add(MailboxAddress.Parse(
options.Sender));
smtpMessage.From.Add(MailboxAddress.Parse(
options.Sender));
smtpMessage.To.Add(MailboxAddress.Parse(
recipient));
smtpMessage.To.Add(MailboxAddress.Parse(
recipient));
smtpMessage.Body = new TextPart(TextFormat.Html)
{
Text = body
};
smtpMessage.Body = new TextPart(TextFormat.Html)
{
Text = body
};
smtpMessage.Subject = subject;
smtpMessage.Subject = subject;
await smtpClient.SendAsync(smtpMessage, ct);
}
await smtpClient.SendAsync(smtpMessage, ct);
}
}
finally

4
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -145,7 +145,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// <summary>
/// The width of the image in pixels if the asset is an image.
/// </summary>
[Obsolete("Use 'metdata' field now.")]
[Obsolete("Use 'metadata' field now.")]
public int? PixelWidth
{
get => Metadata.GetPixelWidth();
@ -154,7 +154,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// <summary>
/// The height of the image in pixels if the asset is an image.
/// </summary>
[Obsolete("Use 'metdata' field now.")]
[Obsolete("Use 'metadata' field now.")]
public int? PixelHeight
{
get => Metadata.GetPixelHeight();

86
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -15,7 +15,6 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
using Squidex.Web.GraphQL;
namespace Squidex.Areas.Api.Controllers.Contents
{
@ -24,97 +23,14 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
private readonly IContentQueryService contentQuery;
private readonly IContentWorkflow contentWorkflow;
private readonly GraphQLRunner graphQLRunner;
public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery,
IContentWorkflow contentWorkflow,
GraphQLRunner graphQLRunner)
IContentWorkflow contentWorkflow)
: base(commandBus)
{
this.contentQuery = contentQuery;
this.contentWorkflow = contentWorkflow;
this.graphQLRunner = graphQLRunner;
}
/// <summary>
/// GraphQL endpoint.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Contents returned or mutated.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/")]
[Route("content/{app}/graphql/batch")]
[ApiPermissionOrAnonymous]
[ApiCosts(2)]
public Task GetGraphQL(string app)
{
return graphQLRunner.InvokeAsync(HttpContext);
}
/// <summary>
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, AllContentsByGetDto query)
{
var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow);
});
return Ok(response);
}
/// <summary>
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPost]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] AllContentsByPostDto query)
{
var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow);
});
return Ok(response);
}
/// <summary>

148
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs

@ -0,0 +1,148 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
using Squidex.Web.GraphQL;
namespace Squidex.Areas.Api.Controllers.Contents
{
[SchemaMustBePublished]
public sealed class ContentsSharedController : ApiController
{
private readonly IContentQueryService contentQuery;
private readonly IContentWorkflow contentWorkflow;
private readonly GraphQLRunner graphQLRunner;
public ContentsSharedController(ICommandBus commandBus,
IContentQueryService contentQuery,
IContentWorkflow contentWorkflow,
GraphQLRunner graphQLRunner)
: base(commandBus)
{
this.contentQuery = contentQuery;
this.contentWorkflow = contentWorkflow;
this.graphQLRunner = graphQLRunner;
}
/// <summary>
/// GraphQL endpoint.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Contents returned or mutated.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/")]
[Route("content/{app}/graphql/batch")]
[ApiPermissionOrAnonymous]
[ApiCosts(2)]
public Task GetGraphQL(string app)
{
return graphQLRunner.InvokeAsync(HttpContext);
}
/// <summary>
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpGet]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, AllContentsByGetDto query)
{
var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow);
});
return Ok(response);
}
/// <summary>
/// Queries contents.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The required query object.</param>
/// <returns>
/// 200 => Contents returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPost]
[Route("content/{app}/")]
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous]
[ApiCosts(1)]
public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] AllContentsByPostDto query)
{
var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted);
var response = Deferred.AsyncResponse(() =>
{
return ContentsDto.FromContentsAsync(contents, Resources, null, contentWorkflow);
});
return Ok(response);
}
/// <summary>
/// Bulk update content items.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="schema">The name of the schema.</param>
/// <param name="request">The bulk update request.</param>
/// <returns>
/// 201 => Contents created, update or delete.
/// 400 => Contents request not valid.
/// 404 => Contents references, schema or app not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPost]
[Route("content/{app}/bulk")]
[ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)]
[ApiCosts(5)]
public async Task<IActionResult> BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request)
{
var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted);
var result = context.Result<BulkUpdateResult>();
var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray();
return Ok(response);
}
}
}

22
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs

@ -513,6 +513,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
.MustHaveHappened();
}
[Fact]
public async Task Should_throw_exception_if_schema_name_not_defined()
{
SetupContext(PermissionIds.AppContentsDeleteOwn);
var (id, _, _) = CreateTestData(false);
var command = BulkCommand(BulkUpdateContentType.Delete, new BulkUpdateJob(), id);
// Unset schema id, so that no schema id is set for the command.
command.SchemaId = null!;
var actual = await PublishAsync(command);
Assert.Single(actual);
Assert.Single(actual, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainObjectNotFoundException);
A.CallTo(() => commandBus.PublishAsync(
A<DeleteContent>.That.Matches(x => x.SchemaId == schemaCustomId), ct))
.MustNotHaveHappened();
}
private async Task<BulkUpdateResult> PublishAsync(ICommand command)
{
var context = new CommandContext(command, commandBus);

Loading…
Cancel
Save