Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
720 lines
26 KiB
C#
720 lines
26 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Notifier.WebService.Contracts;
|
|
using StellaOps.Notifier.Worker.Dispatch;
|
|
using StellaOps.Notifier.Worker.Templates;
|
|
using StellaOps.Notify.Models;
|
|
using StellaOps.Notifier.Worker.Storage;
|
|
using StellaOps.Notifier.WebService.Extensions;
|
|
|
|
namespace StellaOps.Notifier.WebService.Endpoints;
|
|
|
|
/// <summary>
|
|
/// API endpoints for rules, templates, and incidents management.
|
|
/// </summary>
|
|
public static class NotifyApiEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps all Notify API v2 endpoints.
|
|
/// </summary>
|
|
public static IEndpointRouteBuilder MapNotifyApiV2(this IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup("/api/v2/notify")
|
|
.WithTags("Notify")
|
|
.WithOpenApi();
|
|
|
|
// Rules CRUD
|
|
MapRulesEndpoints(group);
|
|
|
|
// Templates CRUD + Preview
|
|
MapTemplatesEndpoints(group);
|
|
|
|
// Incidents
|
|
MapIncidentsEndpoints(group);
|
|
|
|
return app;
|
|
}
|
|
|
|
private static void MapRulesEndpoints(RouteGroupBuilder group)
|
|
{
|
|
group.MapGet("/rules", async (
|
|
HttpContext context,
|
|
INotifyRuleRepository ruleRepository,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var rules = await ruleRepository.ListAsync(tenantId, cancellationToken);
|
|
var response = rules.Select(MapRuleToResponse).ToList();
|
|
|
|
return Results.Ok(response);
|
|
});
|
|
|
|
group.MapGet("/rules/{ruleId}", async (
|
|
HttpContext context,
|
|
string ruleId,
|
|
INotifyRuleRepository ruleRepository,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var rule = await ruleRepository.GetAsync(tenantId, ruleId, cancellationToken);
|
|
if (rule is null)
|
|
{
|
|
return Results.NotFound(Error("rule_not_found", $"Rule {ruleId} not found.", context));
|
|
}
|
|
|
|
return Results.Ok(MapRuleToResponse(rule));
|
|
});
|
|
|
|
group.MapPost("/rules", async (
|
|
HttpContext context,
|
|
RuleCreateRequest request,
|
|
INotifyRuleRepository ruleRepository,
|
|
INotifyAuditRepository auditRepository,
|
|
TimeProvider timeProvider,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var actor = GetActor(context);
|
|
var now = timeProvider.GetUtcNow();
|
|
|
|
var rule = MapRequestToRule(request, tenantId, actor, now);
|
|
|
|
await ruleRepository.UpsertAsync(rule, cancellationToken);
|
|
|
|
await AuditAsync(auditRepository, tenantId, "rule.created", actor, new Dictionary<string, string>
|
|
{
|
|
["ruleId"] = rule.RuleId,
|
|
["name"] = rule.Name
|
|
}, cancellationToken);
|
|
|
|
return Results.Created($"/api/v2/notify/rules/{rule.RuleId}", MapRuleToResponse(rule));
|
|
});
|
|
|
|
group.MapPut("/rules/{ruleId}", async (
|
|
HttpContext context,
|
|
string ruleId,
|
|
RuleUpdateRequest request,
|
|
INotifyRuleRepository ruleRepository,
|
|
INotifyAuditRepository auditRepository,
|
|
TimeProvider timeProvider,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var existing = await ruleRepository.GetAsync(tenantId, ruleId, cancellationToken);
|
|
if (existing is null)
|
|
{
|
|
return Results.NotFound(Error("rule_not_found", $"Rule {ruleId} not found.", context));
|
|
}
|
|
|
|
var actor = GetActor(context);
|
|
var now = timeProvider.GetUtcNow();
|
|
|
|
var updated = ApplyRuleUpdate(existing, request, actor, now);
|
|
|
|
await ruleRepository.UpsertAsync(updated, cancellationToken);
|
|
|
|
await AuditAsync(auditRepository, tenantId, "rule.updated", actor, new Dictionary<string, string>
|
|
{
|
|
["ruleId"] = updated.RuleId,
|
|
["name"] = updated.Name
|
|
}, cancellationToken);
|
|
|
|
return Results.Ok(MapRuleToResponse(updated));
|
|
});
|
|
|
|
group.MapDelete("/rules/{ruleId}", async (
|
|
HttpContext context,
|
|
string ruleId,
|
|
INotifyRuleRepository ruleRepository,
|
|
INotifyAuditRepository auditRepository,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var existing = await ruleRepository.GetAsync(tenantId, ruleId, cancellationToken);
|
|
if (existing is null)
|
|
{
|
|
return Results.NotFound(Error("rule_not_found", $"Rule {ruleId} not found.", context));
|
|
}
|
|
|
|
var actor = GetActor(context);
|
|
|
|
await ruleRepository.DeleteAsync(tenantId, ruleId, cancellationToken);
|
|
|
|
await AuditAsync(auditRepository, tenantId, "rule.deleted", actor, new Dictionary<string, string>
|
|
{
|
|
["ruleId"] = ruleId
|
|
}, cancellationToken);
|
|
|
|
return Results.NoContent();
|
|
});
|
|
}
|
|
|
|
private static void MapTemplatesEndpoints(RouteGroupBuilder group)
|
|
{
|
|
group.MapGet("/templates", async (
|
|
HttpContext context,
|
|
string? keyPrefix,
|
|
string? channelType,
|
|
string? locale,
|
|
int? limit,
|
|
INotifyTemplateService templateService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
NotifyChannelType? channelTypeEnum = null;
|
|
if (!string.IsNullOrWhiteSpace(channelType) &&
|
|
Enum.TryParse<NotifyChannelType>(channelType, true, out var parsed))
|
|
{
|
|
channelTypeEnum = parsed;
|
|
}
|
|
|
|
var templates = await templateService.ListAsync(tenantId, new TemplateListOptions
|
|
{
|
|
KeyPrefix = keyPrefix,
|
|
ChannelType = channelTypeEnum,
|
|
Locale = locale,
|
|
Limit = limit
|
|
}, cancellationToken);
|
|
|
|
var response = templates.Select(MapTemplateToResponse).ToList();
|
|
|
|
return Results.Ok(response);
|
|
});
|
|
|
|
group.MapGet("/templates/{templateId}", async (
|
|
HttpContext context,
|
|
string templateId,
|
|
INotifyTemplateService templateService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var template = await templateService.GetByIdAsync(tenantId, templateId, cancellationToken);
|
|
if (template is null)
|
|
{
|
|
return Results.NotFound(Error("template_not_found", $"Template {templateId} not found.", context));
|
|
}
|
|
|
|
return Results.Ok(MapTemplateToResponse(template));
|
|
});
|
|
|
|
group.MapPost("/templates", async (
|
|
HttpContext context,
|
|
TemplateCreateRequest request,
|
|
INotifyTemplateService templateService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var actor = GetActor(context);
|
|
|
|
if (!Enum.TryParse<NotifyChannelType>(request.ChannelType, true, out var channelType))
|
|
{
|
|
return Results.BadRequest(Error("invalid_channel_type", $"Invalid channel type: {request.ChannelType}", context));
|
|
}
|
|
|
|
var renderMode = NotifyTemplateRenderMode.Markdown;
|
|
if (!string.IsNullOrWhiteSpace(request.RenderMode) &&
|
|
Enum.TryParse<NotifyTemplateRenderMode>(request.RenderMode, true, out var parsedMode))
|
|
{
|
|
renderMode = parsedMode;
|
|
}
|
|
|
|
var format = NotifyDeliveryFormat.Json;
|
|
if (!string.IsNullOrWhiteSpace(request.Format) &&
|
|
Enum.TryParse<NotifyDeliveryFormat>(request.Format, true, out var parsedFormat))
|
|
{
|
|
format = parsedFormat;
|
|
}
|
|
|
|
var template = NotifyTemplate.Create(
|
|
templateId: request.TemplateId,
|
|
tenantId: tenantId,
|
|
channelType: channelType,
|
|
key: request.Key,
|
|
locale: request.Locale,
|
|
body: request.Body,
|
|
renderMode: renderMode,
|
|
format: format,
|
|
description: request.Description,
|
|
metadata: request.Metadata);
|
|
|
|
var result = await templateService.UpsertAsync(template, actor, cancellationToken);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Results.BadRequest(Error("template_validation_failed", result.Error ?? "Validation failed.", context));
|
|
}
|
|
|
|
var created = await templateService.GetByIdAsync(tenantId, request.TemplateId, cancellationToken);
|
|
|
|
return result.IsNew
|
|
? Results.Created($"/api/v2/notify/templates/{request.TemplateId}", MapTemplateToResponse(created!))
|
|
: Results.Ok(MapTemplateToResponse(created!));
|
|
});
|
|
|
|
group.MapDelete("/templates/{templateId}", async (
|
|
HttpContext context,
|
|
string templateId,
|
|
INotifyTemplateService templateService,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var actor = GetActor(context);
|
|
var deleted = await templateService.DeleteAsync(tenantId, templateId, actor, cancellationToken);
|
|
|
|
if (!deleted)
|
|
{
|
|
return Results.NotFound(Error("template_not_found", $"Template {templateId} not found.", context));
|
|
}
|
|
|
|
return Results.NoContent();
|
|
});
|
|
|
|
group.MapPost("/templates/preview", async (
|
|
HttpContext context,
|
|
TemplatePreviewRequest request,
|
|
INotifyTemplateService templateService,
|
|
INotifyTemplateRenderer templateRenderer,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
NotifyTemplate? template = null;
|
|
List<string>? warnings = null;
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.TemplateId))
|
|
{
|
|
template = await templateService.GetByIdAsync(tenantId, request.TemplateId, cancellationToken);
|
|
if (template is null)
|
|
{
|
|
return Results.NotFound(Error("template_not_found", $"Template {request.TemplateId} not found.", context));
|
|
}
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(request.TemplateBody))
|
|
{
|
|
var validation = templateService.Validate(request.TemplateBody);
|
|
if (!validation.IsValid)
|
|
{
|
|
return Results.BadRequest(Error("template_invalid", string.Join("; ", validation.Errors), context));
|
|
}
|
|
warnings = validation.Warnings.ToList();
|
|
|
|
var format = NotifyDeliveryFormat.PlainText;
|
|
if (!string.IsNullOrWhiteSpace(request.OutputFormat) &&
|
|
Enum.TryParse<NotifyDeliveryFormat>(request.OutputFormat, true, out var parsedFormat))
|
|
{
|
|
format = parsedFormat;
|
|
}
|
|
|
|
template = NotifyTemplate.Create(
|
|
templateId: "preview",
|
|
tenantId: tenantId,
|
|
channelType: NotifyChannelType.Custom,
|
|
key: "preview",
|
|
locale: "en-us",
|
|
body: request.TemplateBody,
|
|
format: format);
|
|
}
|
|
else
|
|
{
|
|
return Results.BadRequest(Error("template_required", "Either templateId or templateBody must be provided.", context));
|
|
}
|
|
|
|
var sampleEvent = NotifyEvent.Create(
|
|
eventId: Guid.NewGuid(),
|
|
kind: request.EventKind ?? "preview.event",
|
|
tenant: tenantId,
|
|
ts: DateTimeOffset.UtcNow,
|
|
payload: request.SamplePayload ?? new JsonObject(),
|
|
attributes: request.SampleAttributes ?? new Dictionary<string, string>(),
|
|
actor: "preview",
|
|
version: "1");
|
|
|
|
var rendered = await templateRenderer.RenderAsync(template, sampleEvent, cancellationToken);
|
|
|
|
return Results.Ok(new TemplatePreviewResponse
|
|
{
|
|
RenderedBody = rendered.Body,
|
|
RenderedSubject = rendered.Subject,
|
|
BodyHash = rendered.BodyHash,
|
|
Format = rendered.Format.ToString(),
|
|
Warnings = warnings
|
|
});
|
|
});
|
|
|
|
group.MapPost("/templates/validate", (
|
|
HttpContext context,
|
|
TemplatePreviewRequest request,
|
|
INotifyTemplateService templateService) =>
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.TemplateBody))
|
|
{
|
|
return Results.BadRequest(Error("template_body_required", "templateBody is required.", context));
|
|
}
|
|
|
|
var result = templateService.Validate(request.TemplateBody);
|
|
|
|
return Results.Ok(new
|
|
{
|
|
isValid = result.IsValid,
|
|
errors = result.Errors,
|
|
warnings = result.Warnings
|
|
});
|
|
});
|
|
}
|
|
|
|
private static void MapIncidentsEndpoints(RouteGroupBuilder group)
|
|
{
|
|
group.MapGet("/incidents", async (
|
|
HttpContext context,
|
|
string? status,
|
|
string? eventKindPrefix,
|
|
DateTimeOffset? since,
|
|
DateTimeOffset? until,
|
|
int? limit,
|
|
string? cursor,
|
|
INotifyDeliveryRepository deliveryRepository,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
// For now, return recent deliveries grouped by event kind as "incidents"
|
|
// Full incident correlation will be implemented in NOTIFY-SVC-39-001
|
|
var queryResult = await deliveryRepository.QueryAsync(tenantId, since, status, limit ?? 100, cursor, cancellationToken);
|
|
var deliveries = queryResult.Items;
|
|
|
|
var incidents = deliveries
|
|
.GroupBy(d => d.EventId)
|
|
.Select(g => new IncidentResponse
|
|
{
|
|
IncidentId = g.Key.ToString(),
|
|
TenantId = tenantId,
|
|
EventKind = g.First().Kind,
|
|
Status = g.All(d => d.Status == NotifyDeliveryStatus.Delivered) ? "resolved" : "open",
|
|
Severity = "medium",
|
|
Title = $"Notification: {g.First().Kind}",
|
|
Description = null,
|
|
EventCount = g.Count(),
|
|
FirstOccurrence = g.Min(d => d.CreatedAt),
|
|
LastOccurrence = g.Max(d => d.CreatedAt),
|
|
Labels = null,
|
|
Metadata = null
|
|
})
|
|
.ToList();
|
|
|
|
return Results.Ok(new IncidentListResponse
|
|
{
|
|
Incidents = incidents,
|
|
TotalCount = incidents.Count,
|
|
NextCursor = queryResult.ContinuationToken
|
|
});
|
|
});
|
|
|
|
group.MapPost("/incidents/{incidentId}/ack", async (
|
|
HttpContext context,
|
|
string incidentId,
|
|
IncidentAckRequest request,
|
|
INotifyAuditRepository auditRepository,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var actor = request.Actor ?? GetActor(context);
|
|
|
|
await AuditAsync(auditRepository, tenantId, "incident.acknowledged", actor, new Dictionary<string, string>
|
|
{
|
|
["incidentId"] = incidentId,
|
|
["comment"] = request.Comment ?? ""
|
|
}, cancellationToken);
|
|
|
|
return Results.NoContent();
|
|
});
|
|
|
|
group.MapPost("/incidents/{incidentId}/resolve", async (
|
|
HttpContext context,
|
|
string incidentId,
|
|
IncidentResolveRequest request,
|
|
INotifyAuditRepository auditRepository,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var tenantId = GetTenantId(context);
|
|
if (tenantId is null)
|
|
{
|
|
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
|
}
|
|
|
|
var actor = request.Actor ?? GetActor(context);
|
|
|
|
await AuditAsync(auditRepository, tenantId, "incident.resolved", actor, new Dictionary<string, string>
|
|
{
|
|
["incidentId"] = incidentId,
|
|
["reason"] = request.Reason ?? "",
|
|
["comment"] = request.Comment ?? ""
|
|
}, cancellationToken);
|
|
|
|
return Results.NoContent();
|
|
});
|
|
}
|
|
|
|
#region Helpers
|
|
|
|
private static string? GetTenantId(HttpContext context)
|
|
{
|
|
var value = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
|
return string.IsNullOrWhiteSpace(value) ? null : value;
|
|
}
|
|
|
|
private static string GetActor(HttpContext context)
|
|
{
|
|
return context.Request.Headers["X-StellaOps-Actor"].ToString() is { Length: > 0 } actor
|
|
? actor
|
|
: "api";
|
|
}
|
|
|
|
private static object Error(string code, string message, HttpContext context) => new
|
|
{
|
|
error = new
|
|
{
|
|
code,
|
|
message,
|
|
traceId = context.TraceIdentifier
|
|
}
|
|
};
|
|
|
|
private static async Task AuditAsync(
|
|
INotifyAuditRepository repository,
|
|
string tenantId,
|
|
string action,
|
|
string actor,
|
|
Dictionary<string, string> metadata,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await repository.AppendAsync(tenantId, action, actor, metadata, cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore audit failures
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Mappers
|
|
|
|
private static RuleResponse MapRuleToResponse(NotifyRule rule)
|
|
{
|
|
return new RuleResponse
|
|
{
|
|
RuleId = rule.RuleId,
|
|
TenantId = rule.TenantId,
|
|
Name = rule.Name,
|
|
Description = rule.Description,
|
|
Enabled = rule.Enabled,
|
|
Match = new RuleMatchResponse
|
|
{
|
|
EventKinds = rule.Match.EventKinds.ToList(),
|
|
Namespaces = rule.Match.Namespaces.ToList(),
|
|
Repositories = rule.Match.Repositories.ToList(),
|
|
Digests = rule.Match.Digests.ToList(),
|
|
Labels = rule.Match.Labels.ToList(),
|
|
ComponentPurls = rule.Match.ComponentPurls.ToList(),
|
|
MinSeverity = rule.Match.MinSeverity,
|
|
Verdicts = rule.Match.Verdicts.ToList(),
|
|
KevOnly = rule.Match.KevOnly ?? false
|
|
},
|
|
Actions = rule.Actions.Select(a => new RuleActionResponse
|
|
{
|
|
ActionId = a.ActionId,
|
|
Channel = a.Channel,
|
|
Template = a.Template,
|
|
Digest = a.Digest,
|
|
Throttle = a.Throttle?.ToString(),
|
|
Locale = a.Locale,
|
|
Enabled = a.Enabled,
|
|
Metadata = a.Metadata.ToDictionary(kv => kv.Key, kv => kv.Value)
|
|
}).ToList(),
|
|
Labels = rule.Labels.ToDictionary(kv => kv.Key, kv => kv.Value),
|
|
Metadata = rule.Metadata.ToDictionary(kv => kv.Key, kv => kv.Value),
|
|
CreatedBy = rule.CreatedBy,
|
|
CreatedAt = rule.CreatedAt,
|
|
UpdatedBy = rule.UpdatedBy,
|
|
UpdatedAt = rule.UpdatedAt
|
|
};
|
|
}
|
|
|
|
private static NotifyRule MapRequestToRule(
|
|
RuleCreateRequest request,
|
|
string tenantId,
|
|
string actor,
|
|
DateTimeOffset now)
|
|
{
|
|
var match = NotifyRuleMatch.Create(
|
|
eventKinds: request.Match.EventKinds,
|
|
namespaces: request.Match.Namespaces,
|
|
repositories: request.Match.Repositories,
|
|
digests: request.Match.Digests,
|
|
labels: request.Match.Labels,
|
|
componentPurls: request.Match.ComponentPurls,
|
|
minSeverity: request.Match.MinSeverity,
|
|
verdicts: request.Match.Verdicts,
|
|
kevOnly: request.Match.KevOnly);
|
|
|
|
var actions = request.Actions.Select(a => NotifyRuleAction.Create(
|
|
actionId: a.ActionId,
|
|
channel: a.Channel,
|
|
template: a.Template,
|
|
digest: a.Digest,
|
|
throttle: string.IsNullOrWhiteSpace(a.Throttle) ? null : System.Xml.XmlConvert.ToTimeSpan(a.Throttle),
|
|
locale: a.Locale,
|
|
enabled: a.Enabled,
|
|
metadata: a.Metadata));
|
|
|
|
return NotifyRule.Create(
|
|
ruleId: request.RuleId,
|
|
tenantId: tenantId,
|
|
name: request.Name,
|
|
match: match,
|
|
actions: actions,
|
|
enabled: request.Enabled,
|
|
description: request.Description,
|
|
labels: request.Labels,
|
|
metadata: request.Metadata,
|
|
createdBy: actor,
|
|
createdAt: now,
|
|
updatedBy: actor,
|
|
updatedAt: now);
|
|
}
|
|
|
|
private static NotifyRule ApplyRuleUpdate(
|
|
NotifyRule existing,
|
|
RuleUpdateRequest request,
|
|
string actor,
|
|
DateTimeOffset now)
|
|
{
|
|
var match = request.Match is not null
|
|
? NotifyRuleMatch.Create(
|
|
eventKinds: request.Match.EventKinds ?? existing.Match.EventKinds.ToList(),
|
|
namespaces: request.Match.Namespaces ?? existing.Match.Namespaces.ToList(),
|
|
repositories: request.Match.Repositories ?? existing.Match.Repositories.ToList(),
|
|
digests: request.Match.Digests ?? existing.Match.Digests.ToList(),
|
|
labels: request.Match.Labels ?? existing.Match.Labels.ToList(),
|
|
componentPurls: request.Match.ComponentPurls ?? existing.Match.ComponentPurls.ToList(),
|
|
minSeverity: request.Match.MinSeverity ?? existing.Match.MinSeverity,
|
|
verdicts: request.Match.Verdicts ?? existing.Match.Verdicts.ToList(),
|
|
kevOnly: request.Match.KevOnly ?? existing.Match.KevOnly)
|
|
: existing.Match;
|
|
|
|
var actions = request.Actions is not null
|
|
? request.Actions.Select(a => NotifyRuleAction.Create(
|
|
actionId: a.ActionId,
|
|
channel: a.Channel,
|
|
template: a.Template,
|
|
digest: a.Digest,
|
|
throttle: string.IsNullOrWhiteSpace(a.Throttle) ? null : System.Xml.XmlConvert.ToTimeSpan(a.Throttle),
|
|
locale: a.Locale,
|
|
enabled: a.Enabled,
|
|
metadata: a.Metadata))
|
|
: existing.Actions;
|
|
|
|
return NotifyRule.Create(
|
|
ruleId: existing.RuleId,
|
|
tenantId: existing.TenantId,
|
|
name: request.Name ?? existing.Name,
|
|
match: match,
|
|
actions: actions,
|
|
enabled: request.Enabled ?? existing.Enabled,
|
|
description: request.Description ?? existing.Description,
|
|
labels: request.Labels ?? existing.Labels.ToDictionary(kv => kv.Key, kv => kv.Value),
|
|
metadata: request.Metadata ?? existing.Metadata.ToDictionary(kv => kv.Key, kv => kv.Value),
|
|
createdBy: existing.CreatedBy,
|
|
createdAt: existing.CreatedAt,
|
|
updatedBy: actor,
|
|
updatedAt: now);
|
|
}
|
|
|
|
private static TemplateResponse MapTemplateToResponse(NotifyTemplate template)
|
|
{
|
|
return new TemplateResponse
|
|
{
|
|
TemplateId = template.TemplateId,
|
|
TenantId = template.TenantId,
|
|
Key = template.Key,
|
|
ChannelType = template.ChannelType.ToString(),
|
|
Locale = template.Locale,
|
|
Body = template.Body,
|
|
RenderMode = template.RenderMode.ToString(),
|
|
Format = template.Format.ToString(),
|
|
Description = template.Description,
|
|
Metadata = template.Metadata.ToDictionary(kv => kv.Key, kv => kv.Value),
|
|
CreatedBy = template.CreatedBy,
|
|
CreatedAt = template.CreatedAt,
|
|
UpdatedBy = template.UpdatedBy,
|
|
UpdatedAt = template.UpdatedAt
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
}
|