Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Projections;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Services;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Schedules;
|
||||
|
||||
internal sealed class InMemoryScheduleRepository : IScheduleRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Schedule> _schedules = new(StringComparer.Ordinal);
|
||||
|
||||
public Task UpsertAsync(
|
||||
Schedule schedule,
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_schedules[schedule.Id] = schedule;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<Schedule?> GetAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_schedules.TryGetValue(scheduleId, out var schedule) &&
|
||||
string.Equals(schedule.TenantId, tenantId, StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult<Schedule?>(schedule);
|
||||
}
|
||||
|
||||
return Task.FromResult<Schedule?>(null);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<Schedule>> ListAsync(
|
||||
string tenantId,
|
||||
ScheduleQueryOptions? options = null,
|
||||
MongoDB.Driver.IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new ScheduleQueryOptions();
|
||||
|
||||
var query = _schedules.Values
|
||||
.Where(schedule => string.Equals(schedule.TenantId, tenantId, StringComparison.Ordinal));
|
||||
|
||||
if (!options.IncludeDisabled)
|
||||
{
|
||||
query = query.Where(schedule => schedule.Enabled);
|
||||
}
|
||||
|
||||
var result = query
|
||||
.OrderBy(schedule => schedule.Name, StringComparer.Ordinal)
|
||||
.Take(options.Limit ?? int.MaxValue)
|
||||
.ToArray();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<Schedule>>(result);
|
||||
}
|
||||
|
||||
public Task<bool> SoftDeleteAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
string deletedBy,
|
||||
DateTimeOffset deletedAt,
|
||||
MongoDB.Driver.IClientSessionHandle? session = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_schedules.TryGetValue(scheduleId, out var schedule) &&
|
||||
string.Equals(schedule.TenantId, tenantId, StringComparison.Ordinal))
|
||||
{
|
||||
_schedules.TryRemove(scheduleId, out _);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InMemoryRunSummaryService : IRunSummaryService
|
||||
{
|
||||
private readonly ConcurrentDictionary<(string TenantId, string ScheduleId), RunSummaryProjection> _summaries = new();
|
||||
|
||||
public Task<RunSummaryProjection> ProjectAsync(Run run, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var scheduleId = run.ScheduleId ?? string.Empty;
|
||||
var updatedAt = run.FinishedAt ?? run.StartedAt ?? run.CreatedAt;
|
||||
|
||||
var counters = new RunSummaryCounters(
|
||||
Total: 0,
|
||||
Planning: 0,
|
||||
Queued: 0,
|
||||
Running: 0,
|
||||
Completed: 0,
|
||||
Error: 0,
|
||||
Cancelled: 0,
|
||||
TotalDeltas: 0,
|
||||
TotalNewCriticals: 0,
|
||||
TotalNewHigh: 0,
|
||||
TotalNewMedium: 0,
|
||||
TotalNewLow: 0);
|
||||
|
||||
var projection = new RunSummaryProjection(
|
||||
run.TenantId,
|
||||
scheduleId,
|
||||
updatedAt,
|
||||
null,
|
||||
ImmutableArray<RunSummarySnapshot>.Empty,
|
||||
counters);
|
||||
|
||||
_summaries[(run.TenantId, scheduleId)] = projection;
|
||||
return Task.FromResult(projection);
|
||||
}
|
||||
|
||||
public Task<RunSummaryProjection?> GetAsync(string tenantId, string scheduleId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_summaries.TryGetValue((tenantId, scheduleId), out var projection);
|
||||
return Task.FromResult<RunSummaryProjection?>(projection);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<RunSummaryProjection>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var projections = _summaries.Values
|
||||
.Where(summary => string.Equals(summary.TenantId, tenantId, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
return Task.FromResult<IReadOnlyList<RunSummaryProjection>>(projections);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class InMemorySchedulerAuditService : ISchedulerAuditService
|
||||
{
|
||||
public Task<AuditRecord> WriteAsync(SchedulerAuditEvent auditEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var occurredAt = auditEvent.OccurredAt ?? DateTimeOffset.UtcNow;
|
||||
var record = new AuditRecord(
|
||||
auditEvent.AuditId ?? $"audit_{Guid.NewGuid():N}",
|
||||
auditEvent.TenantId,
|
||||
auditEvent.Category,
|
||||
auditEvent.Action,
|
||||
occurredAt,
|
||||
auditEvent.Actor,
|
||||
auditEvent.EntityId,
|
||||
auditEvent.ScheduleId,
|
||||
auditEvent.RunId,
|
||||
auditEvent.CorrelationId,
|
||||
auditEvent.Metadata?.ToImmutableSortedDictionary(StringComparer.OrdinalIgnoreCase) ?? ImmutableSortedDictionary<string, string>.Empty,
|
||||
auditEvent.Message);
|
||||
|
||||
return Task.FromResult(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Projections;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Schedules;
|
||||
|
||||
internal sealed record ScheduleCreateRequest(
|
||||
[property: JsonPropertyName("name"), Required] string Name,
|
||||
[property: JsonPropertyName("cronExpression"), Required] string CronExpression,
|
||||
[property: JsonPropertyName("timezone"), Required] string Timezone,
|
||||
[property: JsonPropertyName("mode")] ScheduleMode Mode = ScheduleMode.AnalysisOnly,
|
||||
[property: JsonPropertyName("selection"), Required] Selector Selection = null!,
|
||||
[property: JsonPropertyName("onlyIf")] ScheduleOnlyIf? OnlyIf = null,
|
||||
[property: JsonPropertyName("notify")] ScheduleNotify? Notify = null,
|
||||
[property: JsonPropertyName("limits")] ScheduleLimits? Limits = null,
|
||||
[property: JsonPropertyName("subscribers")] ImmutableArray<string>? Subscribers = null,
|
||||
[property: JsonPropertyName("enabled")] bool Enabled = true);
|
||||
|
||||
internal sealed record ScheduleUpdateRequest(
|
||||
[property: JsonPropertyName("name")] string? Name,
|
||||
[property: JsonPropertyName("cronExpression")] string? CronExpression,
|
||||
[property: JsonPropertyName("timezone")] string? Timezone,
|
||||
[property: JsonPropertyName("mode")] ScheduleMode? Mode,
|
||||
[property: JsonPropertyName("selection")] Selector? Selection,
|
||||
[property: JsonPropertyName("onlyIf")] ScheduleOnlyIf? OnlyIf,
|
||||
[property: JsonPropertyName("notify")] ScheduleNotify? Notify,
|
||||
[property: JsonPropertyName("limits")] ScheduleLimits? Limits,
|
||||
[property: JsonPropertyName("subscribers")] ImmutableArray<string>? Subscribers);
|
||||
|
||||
internal sealed record ScheduleCollectionResponse(IReadOnlyList<ScheduleResponse> Schedules);
|
||||
|
||||
internal sealed record ScheduleResponse(Schedule Schedule, RunSummaryProjection? Summary);
|
||||
@@ -0,0 +1,397 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scheduler.Models;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Repositories;
|
||||
using StellaOps.Scheduler.Storage.Mongo.Services;
|
||||
using StellaOps.Scheduler.WebService.Auth;
|
||||
|
||||
namespace StellaOps.Scheduler.WebService.Schedules;
|
||||
|
||||
internal static class ScheduleEndpoints
|
||||
{
|
||||
private const string ReadScope = "scheduler.schedules.read";
|
||||
private const string WriteScope = "scheduler.schedules.write";
|
||||
|
||||
public static IEndpointRouteBuilder MapScheduleEndpoints(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/v1/scheduler/schedules");
|
||||
|
||||
group.MapGet("/", ListSchedulesAsync);
|
||||
group.MapGet("/{scheduleId}", GetScheduleAsync);
|
||||
group.MapPost("/", CreateScheduleAsync);
|
||||
group.MapPatch("/{scheduleId}", UpdateScheduleAsync);
|
||||
group.MapPost("/{scheduleId}/pause", PauseScheduleAsync);
|
||||
group.MapPost("/{scheduleId}/resume", ResumeScheduleAsync);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListSchedulesAsync(
|
||||
HttpContext httpContext,
|
||||
[FromServices] ITenantContextAccessor tenantAccessor,
|
||||
[FromServices] IScopeAuthorizer scopeAuthorizer,
|
||||
[FromServices] IScheduleRepository repository,
|
||||
[FromServices] IRunSummaryService runSummaryService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
scopeAuthorizer.EnsureScope(httpContext, ReadScope);
|
||||
var tenant = tenantAccessor.GetTenant(httpContext);
|
||||
|
||||
var includeDisabled = SchedulerEndpointHelpers.TryParseBoolean(httpContext.Request.Query.TryGetValue("includeDisabled", out var includeDisabledValues) ? includeDisabledValues.ToString() : null);
|
||||
var includeDeleted = SchedulerEndpointHelpers.TryParseBoolean(httpContext.Request.Query.TryGetValue("includeDeleted", out var includeDeletedValues) ? includeDeletedValues.ToString() : null);
|
||||
var limit = SchedulerEndpointHelpers.TryParsePositiveInt(httpContext.Request.Query.TryGetValue("limit", out var limitValues) ? limitValues.ToString() : null);
|
||||
|
||||
var options = new ScheduleQueryOptions
|
||||
{
|
||||
IncludeDisabled = includeDisabled,
|
||||
IncludeDeleted = includeDeleted,
|
||||
Limit = limit
|
||||
};
|
||||
|
||||
var schedules = await repository.ListAsync(tenant.TenantId, options, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var summaries = await runSummaryService.ListAsync(tenant.TenantId, cancellationToken).ConfigureAwait(false);
|
||||
var summaryLookup = summaries.ToDictionary(summary => summary.ScheduleId, summary => summary, StringComparer.Ordinal);
|
||||
|
||||
var response = new ScheduleCollectionResponse(
|
||||
schedules.Select(schedule => new ScheduleResponse(schedule, summaryLookup.GetValueOrDefault(schedule.Id))).ToArray());
|
||||
|
||||
return Results.Ok(response);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or ValidationException)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetScheduleAsync(
|
||||
HttpContext httpContext,
|
||||
string scheduleId,
|
||||
[FromServices] ITenantContextAccessor tenantAccessor,
|
||||
[FromServices] IScopeAuthorizer scopeAuthorizer,
|
||||
[FromServices] IScheduleRepository repository,
|
||||
[FromServices] IRunSummaryService runSummaryService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
scopeAuthorizer.EnsureScope(httpContext, ReadScope);
|
||||
var tenant = tenantAccessor.GetTenant(httpContext);
|
||||
|
||||
var schedule = await repository.GetAsync(tenant.TenantId, scheduleId, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (schedule is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var summary = await runSummaryService.GetAsync(tenant.TenantId, scheduleId, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(new ScheduleResponse(schedule, summary));
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or ValidationException)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateScheduleAsync(
|
||||
HttpContext httpContext,
|
||||
ScheduleCreateRequest request,
|
||||
[FromServices] ITenantContextAccessor tenantAccessor,
|
||||
[FromServices] IScopeAuthorizer scopeAuthorizer,
|
||||
[FromServices] IScheduleRepository repository,
|
||||
[FromServices] ISchedulerAuditService auditService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
[FromServices] ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
scopeAuthorizer.EnsureScope(httpContext, WriteScope);
|
||||
ValidateRequest(request);
|
||||
|
||||
var tenant = tenantAccessor.GetTenant(httpContext);
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var selection = SchedulerEndpointHelpers.NormalizeSelector(request.Selection, tenant.TenantId);
|
||||
var scheduleId = SchedulerEndpointHelpers.GenerateIdentifier("sch");
|
||||
|
||||
var subscribers = request.Subscribers ?? ImmutableArray<string>.Empty;
|
||||
var schedule = new Schedule(
|
||||
scheduleId,
|
||||
tenant.TenantId,
|
||||
request.Name.Trim(),
|
||||
request.Enabled,
|
||||
request.CronExpression.Trim(),
|
||||
request.Timezone.Trim(),
|
||||
request.Mode,
|
||||
selection,
|
||||
request.OnlyIf ?? ScheduleOnlyIf.Default,
|
||||
request.Notify ?? ScheduleNotify.Default,
|
||||
request.Limits ?? ScheduleLimits.Default,
|
||||
subscribers.IsDefault ? ImmutableArray<string>.Empty : subscribers,
|
||||
now,
|
||||
SchedulerEndpointHelpers.ResolveActorId(httpContext),
|
||||
now,
|
||||
SchedulerEndpointHelpers.ResolveActorId(httpContext),
|
||||
SchedulerSchemaVersions.Schedule);
|
||||
|
||||
await repository.UpsertAsync(schedule, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await auditService.WriteAsync(
|
||||
new SchedulerAuditEvent(
|
||||
tenant.TenantId,
|
||||
"scheduler",
|
||||
"create",
|
||||
SchedulerEndpointHelpers.ResolveAuditActor(httpContext),
|
||||
ScheduleId: schedule.Id,
|
||||
Metadata: new Dictionary<string, string>
|
||||
{
|
||||
["cronExpression"] = schedule.CronExpression,
|
||||
["timezone"] = schedule.Timezone
|
||||
}),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new ScheduleResponse(schedule, null);
|
||||
return Results.Created($"/api/v1/scheduler/schedules/{schedule.Id}", response);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or ValidationException)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateScheduleAsync(
|
||||
HttpContext httpContext,
|
||||
string scheduleId,
|
||||
ScheduleUpdateRequest request,
|
||||
[FromServices] ITenantContextAccessor tenantAccessor,
|
||||
[FromServices] IScopeAuthorizer scopeAuthorizer,
|
||||
[FromServices] IScheduleRepository repository,
|
||||
[FromServices] ISchedulerAuditService auditService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
scopeAuthorizer.EnsureScope(httpContext, WriteScope);
|
||||
var tenant = tenantAccessor.GetTenant(httpContext);
|
||||
|
||||
var existing = await repository.GetAsync(tenant.TenantId, scheduleId, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var updated = UpdateSchedule(existing, request, tenant.TenantId, timeProvider.GetUtcNow(), SchedulerEndpointHelpers.ResolveActorId(httpContext));
|
||||
await repository.UpsertAsync(updated, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await auditService.WriteAsync(
|
||||
new SchedulerAuditEvent(
|
||||
tenant.TenantId,
|
||||
"scheduler",
|
||||
"update",
|
||||
SchedulerEndpointHelpers.ResolveAuditActor(httpContext),
|
||||
ScheduleId: updated.Id,
|
||||
Metadata: new Dictionary<string, string>
|
||||
{
|
||||
["updatedAt"] = updated.UpdatedAt.ToString("O")
|
||||
}),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new ScheduleResponse(updated, null));
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or ValidationException)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> PauseScheduleAsync(
|
||||
HttpContext httpContext,
|
||||
string scheduleId,
|
||||
[FromServices] ITenantContextAccessor tenantAccessor,
|
||||
[FromServices] IScopeAuthorizer scopeAuthorizer,
|
||||
[FromServices] IScheduleRepository repository,
|
||||
[FromServices] ISchedulerAuditService auditService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
scopeAuthorizer.EnsureScope(httpContext, WriteScope);
|
||||
var tenant = tenantAccessor.GetTenant(httpContext);
|
||||
|
||||
var existing = await repository.GetAsync(tenant.TenantId, scheduleId, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
if (!existing.Enabled)
|
||||
{
|
||||
return Results.Ok(new ScheduleResponse(existing, null));
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var updated = new Schedule(
|
||||
existing.Id,
|
||||
existing.TenantId,
|
||||
existing.Name,
|
||||
enabled: false,
|
||||
existing.CronExpression,
|
||||
existing.Timezone,
|
||||
existing.Mode,
|
||||
existing.Selection,
|
||||
existing.OnlyIf,
|
||||
existing.Notify,
|
||||
existing.Limits,
|
||||
existing.Subscribers,
|
||||
existing.CreatedAt,
|
||||
existing.CreatedBy,
|
||||
now,
|
||||
SchedulerEndpointHelpers.ResolveActorId(httpContext),
|
||||
existing.SchemaVersion);
|
||||
|
||||
await repository.UpsertAsync(updated, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await auditService.WriteAsync(
|
||||
new SchedulerAuditEvent(
|
||||
tenant.TenantId,
|
||||
"scheduler",
|
||||
"pause",
|
||||
SchedulerEndpointHelpers.ResolveAuditActor(httpContext),
|
||||
ScheduleId: scheduleId,
|
||||
Metadata: new Dictionary<string, string>
|
||||
{
|
||||
["enabled"] = "false"
|
||||
}),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new ScheduleResponse(updated, null));
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or ValidationException)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> ResumeScheduleAsync(
|
||||
HttpContext httpContext,
|
||||
string scheduleId,
|
||||
[FromServices] ITenantContextAccessor tenantAccessor,
|
||||
[FromServices] IScopeAuthorizer scopeAuthorizer,
|
||||
[FromServices] IScheduleRepository repository,
|
||||
[FromServices] ISchedulerAuditService auditService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
scopeAuthorizer.EnsureScope(httpContext, WriteScope);
|
||||
var tenant = tenantAccessor.GetTenant(httpContext);
|
||||
|
||||
var existing = await repository.GetAsync(tenant.TenantId, scheduleId, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
if (existing.Enabled)
|
||||
{
|
||||
return Results.Ok(new ScheduleResponse(existing, null));
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var updated = new Schedule(
|
||||
existing.Id,
|
||||
existing.TenantId,
|
||||
existing.Name,
|
||||
enabled: true,
|
||||
existing.CronExpression,
|
||||
existing.Timezone,
|
||||
existing.Mode,
|
||||
existing.Selection,
|
||||
existing.OnlyIf,
|
||||
existing.Notify,
|
||||
existing.Limits,
|
||||
existing.Subscribers,
|
||||
existing.CreatedAt,
|
||||
existing.CreatedBy,
|
||||
now,
|
||||
SchedulerEndpointHelpers.ResolveActorId(httpContext),
|
||||
existing.SchemaVersion);
|
||||
|
||||
await repository.UpsertAsync(updated, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await auditService.WriteAsync(
|
||||
new SchedulerAuditEvent(
|
||||
tenant.TenantId,
|
||||
"scheduler",
|
||||
"resume",
|
||||
SchedulerEndpointHelpers.ResolveAuditActor(httpContext),
|
||||
ScheduleId: scheduleId,
|
||||
Metadata: new Dictionary<string, string>
|
||||
{
|
||||
["enabled"] = "true"
|
||||
}),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new ScheduleResponse(updated, null));
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or ValidationException)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRequest(ScheduleCreateRequest request)
|
||||
{
|
||||
if (request.Selection is null)
|
||||
{
|
||||
throw new ValidationException("Selection is required.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Schedule UpdateSchedule(
|
||||
Schedule existing,
|
||||
ScheduleUpdateRequest request,
|
||||
string tenantId,
|
||||
DateTimeOffset updatedAt,
|
||||
string actor)
|
||||
{
|
||||
var name = request.Name?.Trim() ?? existing.Name;
|
||||
var cronExpression = request.CronExpression?.Trim() ?? existing.CronExpression;
|
||||
var timezone = request.Timezone?.Trim() ?? existing.Timezone;
|
||||
var mode = request.Mode ?? existing.Mode;
|
||||
var selection = request.Selection is null
|
||||
? existing.Selection
|
||||
: SchedulerEndpointHelpers.NormalizeSelector(request.Selection, tenantId);
|
||||
var onlyIf = request.OnlyIf ?? existing.OnlyIf;
|
||||
var notify = request.Notify ?? existing.Notify;
|
||||
var limits = request.Limits ?? existing.Limits;
|
||||
var subscribers = request.Subscribers ?? existing.Subscribers;
|
||||
|
||||
return new Schedule(
|
||||
existing.Id,
|
||||
existing.TenantId,
|
||||
name,
|
||||
existing.Enabled,
|
||||
cronExpression,
|
||||
timezone,
|
||||
mode,
|
||||
selection,
|
||||
onlyIf,
|
||||
notify,
|
||||
limits,
|
||||
subscribers.IsDefault ? ImmutableArray<string>.Empty : subscribers,
|
||||
existing.CreatedAt,
|
||||
existing.CreatedBy,
|
||||
updatedAt,
|
||||
actor,
|
||||
existing.SchemaVersion);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user