up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Digest status values.
|
||||
/// </summary>
|
||||
public static class DigestStatus
|
||||
{
|
||||
public const string Collecting = "collecting";
|
||||
public const string Sending = "sending";
|
||||
public const string Sent = "sent";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a digest of aggregated notifications.
|
||||
/// </summary>
|
||||
public sealed class DigestEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required Guid ChannelId { get; init; }
|
||||
public required string Recipient { get; init; }
|
||||
public required string DigestKey { get; init; }
|
||||
public int EventCount { get; init; }
|
||||
public string Events { get; init; } = "[]";
|
||||
public string Status { get; init; } = DigestStatus.Collecting;
|
||||
public DateTimeOffset CollectUntil { get; init; }
|
||||
public DateTimeOffset? SentAt { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an escalation policy.
|
||||
/// </summary>
|
||||
public sealed class EscalationPolicyEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public bool Enabled { get; init; } = true;
|
||||
public string Steps { get; init; } = "[]";
|
||||
public int RepeatCount { get; init; }
|
||||
public string Metadata { get; init; } = "{}";
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escalation state status values.
|
||||
/// </summary>
|
||||
public static class EscalationStatus
|
||||
{
|
||||
public const string Active = "active";
|
||||
public const string Acknowledged = "acknowledged";
|
||||
public const string Resolved = "resolved";
|
||||
public const string Expired = "expired";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of an escalation.
|
||||
/// </summary>
|
||||
public sealed class EscalationStateEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required Guid PolicyId { get; init; }
|
||||
public Guid? IncidentId { get; init; }
|
||||
public required string CorrelationId { get; init; }
|
||||
public int CurrentStep { get; init; }
|
||||
public int RepeatIteration { get; init; }
|
||||
public string Status { get; init; } = EscalationStatus.Active;
|
||||
public DateTimeOffset StartedAt { get; init; }
|
||||
public DateTimeOffset? NextEscalationAt { get; init; }
|
||||
public DateTimeOffset? AcknowledgedAt { get; init; }
|
||||
public string? AcknowledgedBy { get; init; }
|
||||
public DateTimeOffset? ResolvedAt { get; init; }
|
||||
public string? ResolvedBy { get; init; }
|
||||
public string Metadata { get; init; } = "{}";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an in-app notification inbox item.
|
||||
/// </summary>
|
||||
public sealed class InboxEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required Guid UserId { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string? Body { get; init; }
|
||||
public required string EventType { get; init; }
|
||||
public string EventPayload { get; init; } = "{}";
|
||||
public bool Read { get; init; }
|
||||
public bool Archived { get; init; }
|
||||
public string? ActionUrl { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? ReadAt { get; init; }
|
||||
public DateTimeOffset? ArchivedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Incident severity values.
|
||||
/// </summary>
|
||||
public static class IncidentSeverity
|
||||
{
|
||||
public const string Critical = "critical";
|
||||
public const string High = "high";
|
||||
public const string Medium = "medium";
|
||||
public const string Low = "low";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Incident status values.
|
||||
/// </summary>
|
||||
public static class IncidentStatus
|
||||
{
|
||||
public const string Open = "open";
|
||||
public const string Acknowledged = "acknowledged";
|
||||
public const string Resolved = "resolved";
|
||||
public const string Closed = "closed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an incident.
|
||||
/// </summary>
|
||||
public sealed class IncidentEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string Severity { get; init; } = IncidentSeverity.Medium;
|
||||
public string Status { get; init; } = IncidentStatus.Open;
|
||||
public string? Source { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
public Guid? AssignedTo { get; init; }
|
||||
public Guid? EscalationPolicyId { get; init; }
|
||||
public string Metadata { get; init; } = "{}";
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? AcknowledgedAt { get; init; }
|
||||
public DateTimeOffset? ResolvedAt { get; init; }
|
||||
public DateTimeOffset? ClosedAt { get; init; }
|
||||
public string? CreatedBy { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a maintenance window for suppressing notifications.
|
||||
/// </summary>
|
||||
public sealed class MaintenanceWindowEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public DateTimeOffset StartAt { get; init; }
|
||||
public DateTimeOffset EndAt { get; init; }
|
||||
public Guid[]? SuppressChannels { get; init; }
|
||||
public string[]? SuppressEventTypes { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public string? CreatedBy { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an audit log entry for the notify module.
|
||||
/// </summary>
|
||||
public sealed class NotifyAuditEntity
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public Guid? UserId { get; init; }
|
||||
public required string Action { get; init; }
|
||||
public required string ResourceType { get; init; }
|
||||
public string? ResourceId { get; init; }
|
||||
public string? Details { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Rotation type values.
|
||||
/// </summary>
|
||||
public static class RotationType
|
||||
{
|
||||
public const string Daily = "daily";
|
||||
public const string Weekly = "weekly";
|
||||
public const string Custom = "custom";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an on-call schedule.
|
||||
/// </summary>
|
||||
public sealed class OnCallScheduleEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string Timezone { get; init; } = "UTC";
|
||||
public string RotationType { get; init; } = Models.RotationType.Weekly;
|
||||
public string Participants { get; init; } = "[]";
|
||||
public string Overrides { get; init; } = "[]";
|
||||
public string Metadata { get; init; } = "{}";
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents quiet hours configuration.
|
||||
/// </summary>
|
||||
public sealed class QuietHoursEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public Guid? UserId { get; init; }
|
||||
public Guid? ChannelId { get; init; }
|
||||
public required TimeOnly StartTime { get; init; }
|
||||
public required TimeOnly EndTime { get; init; }
|
||||
public string Timezone { get; init; } = "UTC";
|
||||
public int[] DaysOfWeek { get; init; } = [0, 1, 2, 3, 4, 5, 6];
|
||||
public bool Enabled { get; init; } = true;
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a notification routing rule.
|
||||
/// </summary>
|
||||
public sealed class RuleEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public bool Enabled { get; init; } = true;
|
||||
public int Priority { get; init; }
|
||||
public string[] EventTypes { get; init; } = [];
|
||||
public string Filter { get; init; } = "{}";
|
||||
public Guid[] ChannelIds { get; init; } = [];
|
||||
public Guid? TemplateId { get; init; }
|
||||
public string Metadata { get; init; } = "{}";
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a notification template.
|
||||
/// </summary>
|
||||
public sealed class TemplateEntity
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required ChannelType ChannelType { get; init; }
|
||||
public string? SubjectTemplate { get; init; }
|
||||
public required string BodyTemplate { get; init; }
|
||||
public string Locale { get; init; } = "en";
|
||||
public string Metadata { get; init; } = "{}";
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class DigestRepository : RepositoryBase<NotifyDataSource>, IDigestRepository
|
||||
{
|
||||
public DigestRepository(NotifyDataSource dataSource, ILogger<DigestRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<DigestEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, channel_id, recipient, digest_key, event_count, events, status, collect_until, sent_at, created_at, updated_at
|
||||
FROM notify.digests WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapDigest, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<DigestEntity?> GetByKeyAsync(string tenantId, Guid channelId, string recipient, string digestKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, channel_id, recipient, digest_key, event_count, events, status, collect_until, sent_at, created_at, updated_at
|
||||
FROM notify.digests WHERE tenant_id = @tenant_id AND channel_id = @channel_id AND recipient = @recipient AND digest_key = @digest_key
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "channel_id", channelId);
|
||||
AddParameter(cmd, "recipient", recipient);
|
||||
AddParameter(cmd, "digest_key", digestKey);
|
||||
}, MapDigest, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DigestEntity>> GetReadyToSendAsync(int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, channel_id, recipient, digest_key, event_count, events, status, collect_until, sent_at, created_at, updated_at
|
||||
FROM notify.digests WHERE status = 'collecting' AND collect_until <= NOW()
|
||||
ORDER BY collect_until LIMIT @limit
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "limit", limit);
|
||||
var results = new List<DigestEntity>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
results.Add(MapDigest(reader));
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<DigestEntity> UpsertAsync(DigestEntity digest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.digests (id, tenant_id, channel_id, recipient, digest_key, event_count, events, status, collect_until)
|
||||
VALUES (@id, @tenant_id, @channel_id, @recipient, @digest_key, @event_count, @events::jsonb, @status, @collect_until)
|
||||
ON CONFLICT (tenant_id, channel_id, recipient, digest_key) DO UPDATE SET
|
||||
event_count = notify.digests.event_count + EXCLUDED.event_count,
|
||||
events = notify.digests.events || EXCLUDED.events,
|
||||
collect_until = GREATEST(notify.digests.collect_until, EXCLUDED.collect_until)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = digest.Id == Guid.Empty ? Guid.NewGuid() : digest.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(digest.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", digest.TenantId);
|
||||
AddParameter(command, "channel_id", digest.ChannelId);
|
||||
AddParameter(command, "recipient", digest.Recipient);
|
||||
AddParameter(command, "digest_key", digest.DigestKey);
|
||||
AddParameter(command, "event_count", digest.EventCount);
|
||||
AddJsonbParameter(command, "events", digest.Events);
|
||||
AddParameter(command, "status", digest.Status);
|
||||
AddParameter(command, "collect_until", digest.CollectUntil);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapDigest(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> AddEventAsync(string tenantId, Guid id, string eventJson, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.digests SET event_count = event_count + 1, events = events || @event::jsonb
|
||||
WHERE tenant_id = @tenant_id AND id = @id AND status = 'collecting'
|
||||
""";
|
||||
var rows = await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddJsonbParameter(cmd, "event", eventJson);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> MarkSendingAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.digests SET status = 'sending' WHERE tenant_id = @tenant_id AND id = @id AND status = 'collecting'";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> MarkSentAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.digests SET status = 'sent', sent_at = NOW() WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.digests WHERE status = 'sent' AND sent_at < @cutoff";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "cutoff", cutoff);
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static DigestEntity MapDigest(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
ChannelId = reader.GetGuid(2),
|
||||
Recipient = reader.GetString(3),
|
||||
DigestKey = reader.GetString(4),
|
||||
EventCount = reader.GetInt32(5),
|
||||
Events = reader.GetString(6),
|
||||
Status = reader.GetString(7),
|
||||
CollectUntil = reader.GetFieldValue<DateTimeOffset>(8),
|
||||
SentAt = GetNullableDateTimeOffset(reader, 9),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(10),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(11)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class EscalationPolicyRepository : RepositoryBase<NotifyDataSource>, IEscalationPolicyRepository
|
||||
{
|
||||
public EscalationPolicyRepository(NotifyDataSource dataSource, ILogger<EscalationPolicyRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<EscalationPolicyEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, enabled, steps, repeat_count, metadata, created_at, updated_at
|
||||
FROM notify.escalation_policies WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapPolicy, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<EscalationPolicyEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, enabled, steps, repeat_count, metadata, created_at, updated_at
|
||||
FROM notify.escalation_policies WHERE tenant_id = @tenant_id AND name = @name
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "name", name); },
|
||||
MapPolicy, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EscalationPolicyEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, enabled, steps, repeat_count, metadata, created_at, updated_at
|
||||
FROM notify.escalation_policies WHERE tenant_id = @tenant_id ORDER BY name
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapPolicy, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<EscalationPolicyEntity> CreateAsync(EscalationPolicyEntity policy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.escalation_policies (id, tenant_id, name, description, enabled, steps, repeat_count, metadata)
|
||||
VALUES (@id, @tenant_id, @name, @description, @enabled, @steps::jsonb, @repeat_count, @metadata::jsonb)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = policy.Id == Guid.Empty ? Guid.NewGuid() : policy.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(policy.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", policy.TenantId);
|
||||
AddParameter(command, "name", policy.Name);
|
||||
AddParameter(command, "description", policy.Description);
|
||||
AddParameter(command, "enabled", policy.Enabled);
|
||||
AddJsonbParameter(command, "steps", policy.Steps);
|
||||
AddParameter(command, "repeat_count", policy.RepeatCount);
|
||||
AddJsonbParameter(command, "metadata", policy.Metadata);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapPolicy(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(EscalationPolicyEntity policy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.escalation_policies SET name = @name, description = @description, enabled = @enabled,
|
||||
steps = @steps::jsonb, repeat_count = @repeat_count, metadata = @metadata::jsonb
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
var rows = await ExecuteAsync(policy.TenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", policy.TenantId);
|
||||
AddParameter(cmd, "id", policy.Id);
|
||||
AddParameter(cmd, "name", policy.Name);
|
||||
AddParameter(cmd, "description", policy.Description);
|
||||
AddParameter(cmd, "enabled", policy.Enabled);
|
||||
AddJsonbParameter(cmd, "steps", policy.Steps);
|
||||
AddParameter(cmd, "repeat_count", policy.RepeatCount);
|
||||
AddJsonbParameter(cmd, "metadata", policy.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.escalation_policies WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static EscalationPolicyEntity MapPolicy(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
Description = GetNullableString(reader, 3),
|
||||
Enabled = reader.GetBoolean(4),
|
||||
Steps = reader.GetString(5),
|
||||
RepeatCount = reader.GetInt32(6),
|
||||
Metadata = reader.GetString(7),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(8),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(9)
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class EscalationStateRepository : RepositoryBase<NotifyDataSource>, IEscalationStateRepository
|
||||
{
|
||||
public EscalationStateRepository(NotifyDataSource dataSource, ILogger<EscalationStateRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<EscalationStateEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, policy_id, incident_id, correlation_id, current_step, repeat_iteration, status,
|
||||
started_at, next_escalation_at, acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata
|
||||
FROM notify.escalation_states WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapState, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<EscalationStateEntity?> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, policy_id, incident_id, correlation_id, current_step, repeat_iteration, status,
|
||||
started_at, next_escalation_at, acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata
|
||||
FROM notify.escalation_states WHERE tenant_id = @tenant_id AND correlation_id = @correlation_id AND status = 'active'
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "correlation_id", correlationId); },
|
||||
MapState, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EscalationStateEntity>> GetActiveAsync(int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, policy_id, incident_id, correlation_id, current_step, repeat_iteration, status,
|
||||
started_at, next_escalation_at, acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata
|
||||
FROM notify.escalation_states WHERE status = 'active' AND next_escalation_at <= NOW()
|
||||
ORDER BY next_escalation_at LIMIT @limit
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "limit", limit);
|
||||
var results = new List<EscalationStateEntity>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
results.Add(MapState(reader));
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<EscalationStateEntity> CreateAsync(EscalationStateEntity state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.escalation_states (id, tenant_id, policy_id, incident_id, correlation_id, current_step, repeat_iteration, status, next_escalation_at, metadata)
|
||||
VALUES (@id, @tenant_id, @policy_id, @incident_id, @correlation_id, @current_step, @repeat_iteration, @status, @next_escalation_at, @metadata::jsonb)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = state.Id == Guid.Empty ? Guid.NewGuid() : state.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(state.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", state.TenantId);
|
||||
AddParameter(command, "policy_id", state.PolicyId);
|
||||
AddParameter(command, "incident_id", state.IncidentId);
|
||||
AddParameter(command, "correlation_id", state.CorrelationId);
|
||||
AddParameter(command, "current_step", state.CurrentStep);
|
||||
AddParameter(command, "repeat_iteration", state.RepeatIteration);
|
||||
AddParameter(command, "status", state.Status);
|
||||
AddParameter(command, "next_escalation_at", state.NextEscalationAt);
|
||||
AddJsonbParameter(command, "metadata", state.Metadata);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapState(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> EscalateAsync(string tenantId, Guid id, int newStep, DateTimeOffset? nextEscalationAt, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.escalation_states SET current_step = @new_step, next_escalation_at = @next_escalation_at
|
||||
WHERE tenant_id = @tenant_id AND id = @id AND status = 'active'
|
||||
""";
|
||||
var rows = await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "new_step", newStep);
|
||||
AddParameter(cmd, "next_escalation_at", nextEscalationAt);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> AcknowledgeAsync(string tenantId, Guid id, string acknowledgedBy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.escalation_states SET status = 'acknowledged', acknowledged_at = NOW(), acknowledged_by = @acknowledged_by
|
||||
WHERE tenant_id = @tenant_id AND id = @id AND status = 'active'
|
||||
""";
|
||||
var rows = await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "acknowledged_by", acknowledgedBy);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> ResolveAsync(string tenantId, Guid id, string resolvedBy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.escalation_states SET status = 'resolved', resolved_at = NOW(), resolved_by = @resolved_by
|
||||
WHERE tenant_id = @tenant_id AND id = @id AND status IN ('active', 'acknowledged')
|
||||
""";
|
||||
var rows = await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "resolved_by", resolvedBy);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static EscalationStateEntity MapState(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
PolicyId = reader.GetGuid(2),
|
||||
IncidentId = GetNullableGuid(reader, 3),
|
||||
CorrelationId = reader.GetString(4),
|
||||
CurrentStep = reader.GetInt32(5),
|
||||
RepeatIteration = reader.GetInt32(6),
|
||||
Status = reader.GetString(7),
|
||||
StartedAt = reader.GetFieldValue<DateTimeOffset>(8),
|
||||
NextEscalationAt = GetNullableDateTimeOffset(reader, 9),
|
||||
AcknowledgedAt = GetNullableDateTimeOffset(reader, 10),
|
||||
AcknowledgedBy = GetNullableString(reader, 11),
|
||||
ResolvedAt = GetNullableDateTimeOffset(reader, 12),
|
||||
ResolvedBy = GetNullableString(reader, 13),
|
||||
Metadata = reader.GetString(14)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IDigestRepository
|
||||
{
|
||||
Task<DigestEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<DigestEntity?> GetByKeyAsync(string tenantId, Guid channelId, string recipient, string digestKey, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<DigestEntity>> GetReadyToSendAsync(int limit = 100, CancellationToken cancellationToken = default);
|
||||
Task<DigestEntity> UpsertAsync(DigestEntity digest, CancellationToken cancellationToken = default);
|
||||
Task<bool> AddEventAsync(string tenantId, Guid id, string eventJson, CancellationToken cancellationToken = default);
|
||||
Task<bool> MarkSendingAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<bool> MarkSentAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IEscalationPolicyRepository
|
||||
{
|
||||
Task<EscalationPolicyEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<EscalationPolicyEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EscalationPolicyEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<EscalationPolicyEntity> CreateAsync(EscalationPolicyEntity policy, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(EscalationPolicyEntity policy, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IEscalationStateRepository
|
||||
{
|
||||
Task<EscalationStateEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<EscalationStateEntity?> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EscalationStateEntity>> GetActiveAsync(int limit = 100, CancellationToken cancellationToken = default);
|
||||
Task<EscalationStateEntity> CreateAsync(EscalationStateEntity state, CancellationToken cancellationToken = default);
|
||||
Task<bool> EscalateAsync(string tenantId, Guid id, int newStep, DateTimeOffset? nextEscalationAt, CancellationToken cancellationToken = default);
|
||||
Task<bool> AcknowledgeAsync(string tenantId, Guid id, string acknowledgedBy, CancellationToken cancellationToken = default);
|
||||
Task<bool> ResolveAsync(string tenantId, Guid id, string resolvedBy, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IInboxRepository
|
||||
{
|
||||
Task<InboxEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<InboxEntity>> GetForUserAsync(string tenantId, Guid userId, bool unreadOnly = false, int limit = 50, int offset = 0, CancellationToken cancellationToken = default);
|
||||
Task<int> GetUnreadCountAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default);
|
||||
Task<InboxEntity> CreateAsync(InboxEntity inbox, CancellationToken cancellationToken = default);
|
||||
Task<bool> MarkReadAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<int> MarkAllReadAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default);
|
||||
Task<bool> ArchiveAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IIncidentRepository
|
||||
{
|
||||
Task<IncidentEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IncidentEntity?> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<IncidentEntity>> ListAsync(string tenantId, string? status = null, string? severity = null, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||
Task<IncidentEntity> CreateAsync(IncidentEntity incident, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(IncidentEntity incident, CancellationToken cancellationToken = default);
|
||||
Task<bool> AcknowledgeAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<bool> ResolveAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<bool> CloseAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<bool> AssignAsync(string tenantId, Guid id, Guid assignedTo, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IMaintenanceWindowRepository
|
||||
{
|
||||
Task<MaintenanceWindowEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<MaintenanceWindowEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<MaintenanceWindowEntity>> GetActiveAsync(string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<MaintenanceWindowEntity> CreateAsync(MaintenanceWindowEntity window, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(MaintenanceWindowEntity window, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteExpiredAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface INotifyAuditRepository
|
||||
{
|
||||
Task<long> CreateAsync(NotifyAuditEntity audit, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NotifyAuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NotifyAuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId = null, int limit = 100, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NotifyAuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IOnCallScheduleRepository
|
||||
{
|
||||
Task<OnCallScheduleEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<OnCallScheduleEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<OnCallScheduleEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<OnCallScheduleEntity> CreateAsync(OnCallScheduleEntity schedule, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(OnCallScheduleEntity schedule, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IQuietHoursRepository
|
||||
{
|
||||
Task<QuietHoursEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<QuietHoursEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<QuietHoursEntity>> GetForUserAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default);
|
||||
Task<QuietHoursEntity> CreateAsync(QuietHoursEntity quietHours, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(QuietHoursEntity quietHours, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface IRuleRepository
|
||||
{
|
||||
Task<RuleEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<RuleEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<RuleEntity>> ListAsync(string tenantId, bool? enabled = null, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<RuleEntity>> GetMatchingRulesAsync(string tenantId, string eventType, CancellationToken cancellationToken = default);
|
||||
Task<RuleEntity> CreateAsync(RuleEntity rule, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(RuleEntity rule, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public interface ITemplateRepository
|
||||
{
|
||||
Task<TemplateEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
Task<TemplateEntity?> GetByNameAsync(string tenantId, string name, ChannelType channelType, string locale = "en", CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<TemplateEntity>> ListAsync(string tenantId, ChannelType? channelType = null, CancellationToken cancellationToken = default);
|
||||
Task<TemplateEntity> CreateAsync(TemplateEntity template, CancellationToken cancellationToken = default);
|
||||
Task<bool> UpdateAsync(TemplateEntity template, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class InboxRepository : RepositoryBase<NotifyDataSource>, IInboxRepository
|
||||
{
|
||||
public InboxRepository(NotifyDataSource dataSource, ILogger<InboxRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<InboxEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, title, body, event_type, event_payload, read, archived, action_url, correlation_id, created_at, read_at, archived_at
|
||||
FROM notify.inbox WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapInbox, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<InboxEntity>> GetForUserAsync(string tenantId, Guid userId, bool unreadOnly = false, int limit = 50, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant_id, user_id, title, body, event_type, event_payload, read, archived, action_url, correlation_id, created_at, read_at, archived_at
|
||||
FROM notify.inbox WHERE tenant_id = @tenant_id AND user_id = @user_id AND archived = FALSE
|
||||
""";
|
||||
if (unreadOnly) sql += " AND read = FALSE";
|
||||
sql += " ORDER BY created_at DESC LIMIT @limit OFFSET @offset";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "user_id", userId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
}, MapInbox, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> GetUnreadCountAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "SELECT COUNT(*) FROM notify.inbox WHERE tenant_id = @tenant_id AND user_id = @user_id AND read = FALSE AND archived = FALSE";
|
||||
await using var connection = await DataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "tenant_id", tenantId);
|
||||
AddParameter(command, "user_id", userId);
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToInt32(result);
|
||||
}
|
||||
|
||||
public async Task<InboxEntity> CreateAsync(InboxEntity inbox, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.inbox (id, tenant_id, user_id, title, body, event_type, event_payload, action_url, correlation_id)
|
||||
VALUES (@id, @tenant_id, @user_id, @title, @body, @event_type, @event_payload::jsonb, @action_url, @correlation_id)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = inbox.Id == Guid.Empty ? Guid.NewGuid() : inbox.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(inbox.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", inbox.TenantId);
|
||||
AddParameter(command, "user_id", inbox.UserId);
|
||||
AddParameter(command, "title", inbox.Title);
|
||||
AddParameter(command, "body", inbox.Body);
|
||||
AddParameter(command, "event_type", inbox.EventType);
|
||||
AddJsonbParameter(command, "event_payload", inbox.EventPayload);
|
||||
AddParameter(command, "action_url", inbox.ActionUrl);
|
||||
AddParameter(command, "correlation_id", inbox.CorrelationId);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapInbox(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> MarkReadAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.inbox SET read = TRUE, read_at = NOW() WHERE tenant_id = @tenant_id AND id = @id AND read = FALSE";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<int> MarkAllReadAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.inbox SET read = TRUE, read_at = NOW() WHERE tenant_id = @tenant_id AND user_id = @user_id AND read = FALSE";
|
||||
return await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> ArchiveAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.inbox SET archived = TRUE, archived_at = NOW() WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.inbox WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.inbox WHERE archived = TRUE AND archived_at < @cutoff";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "cutoff", cutoff);
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static InboxEntity MapInbox(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = reader.GetGuid(2),
|
||||
Title = reader.GetString(3),
|
||||
Body = GetNullableString(reader, 4),
|
||||
EventType = reader.GetString(5),
|
||||
EventPayload = reader.GetString(6),
|
||||
Read = reader.GetBoolean(7),
|
||||
Archived = reader.GetBoolean(8),
|
||||
ActionUrl = GetNullableString(reader, 9),
|
||||
CorrelationId = GetNullableString(reader, 10),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11),
|
||||
ReadAt = GetNullableDateTimeOffset(reader, 12),
|
||||
ArchivedAt = GetNullableDateTimeOffset(reader, 13)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class IncidentRepository : RepositoryBase<NotifyDataSource>, IIncidentRepository
|
||||
{
|
||||
public IncidentRepository(NotifyDataSource dataSource, ILogger<IncidentRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<IncidentEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, title, description, severity, status, source, correlation_id, assigned_to, escalation_policy_id,
|
||||
metadata, created_at, acknowledged_at, resolved_at, closed_at, created_by
|
||||
FROM notify.incidents WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapIncident, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IncidentEntity?> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, title, description, severity, status, source, correlation_id, assigned_to, escalation_policy_id,
|
||||
metadata, created_at, acknowledged_at, resolved_at, closed_at, created_by
|
||||
FROM notify.incidents WHERE tenant_id = @tenant_id AND correlation_id = @correlation_id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "correlation_id", correlationId); },
|
||||
MapIncident, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<IncidentEntity>> ListAsync(string tenantId, string? status = null, string? severity = null, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant_id, title, description, severity, status, source, correlation_id, assigned_to, escalation_policy_id,
|
||||
metadata, created_at, acknowledged_at, resolved_at, closed_at, created_by
|
||||
FROM notify.incidents WHERE tenant_id = @tenant_id
|
||||
""";
|
||||
if (status != null) sql += " AND status = @status";
|
||||
if (severity != null) sql += " AND severity = @severity";
|
||||
sql += " ORDER BY created_at DESC LIMIT @limit OFFSET @offset";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
if (status != null) AddParameter(cmd, "status", status);
|
||||
if (severity != null) AddParameter(cmd, "severity", severity);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
}, MapIncident, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IncidentEntity> CreateAsync(IncidentEntity incident, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.incidents (id, tenant_id, title, description, severity, status, source, correlation_id, assigned_to, escalation_policy_id, metadata, created_by)
|
||||
VALUES (@id, @tenant_id, @title, @description, @severity, @status, @source, @correlation_id, @assigned_to, @escalation_policy_id, @metadata::jsonb, @created_by)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = incident.Id == Guid.Empty ? Guid.NewGuid() : incident.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(incident.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", incident.TenantId);
|
||||
AddParameter(command, "title", incident.Title);
|
||||
AddParameter(command, "description", incident.Description);
|
||||
AddParameter(command, "severity", incident.Severity);
|
||||
AddParameter(command, "status", incident.Status);
|
||||
AddParameter(command, "source", incident.Source);
|
||||
AddParameter(command, "correlation_id", incident.CorrelationId);
|
||||
AddParameter(command, "assigned_to", incident.AssignedTo);
|
||||
AddParameter(command, "escalation_policy_id", incident.EscalationPolicyId);
|
||||
AddJsonbParameter(command, "metadata", incident.Metadata);
|
||||
AddParameter(command, "created_by", incident.CreatedBy);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapIncident(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(IncidentEntity incident, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.incidents SET title = @title, description = @description, severity = @severity, status = @status,
|
||||
source = @source, assigned_to = @assigned_to, escalation_policy_id = @escalation_policy_id, metadata = @metadata::jsonb
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
var rows = await ExecuteAsync(incident.TenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", incident.TenantId);
|
||||
AddParameter(cmd, "id", incident.Id);
|
||||
AddParameter(cmd, "title", incident.Title);
|
||||
AddParameter(cmd, "description", incident.Description);
|
||||
AddParameter(cmd, "severity", incident.Severity);
|
||||
AddParameter(cmd, "status", incident.Status);
|
||||
AddParameter(cmd, "source", incident.Source);
|
||||
AddParameter(cmd, "assigned_to", incident.AssignedTo);
|
||||
AddParameter(cmd, "escalation_policy_id", incident.EscalationPolicyId);
|
||||
AddJsonbParameter(cmd, "metadata", incident.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> AcknowledgeAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.incidents SET status = 'acknowledged', acknowledged_at = NOW() WHERE tenant_id = @tenant_id AND id = @id AND status = 'open'";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> ResolveAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.incidents SET status = 'resolved', resolved_at = NOW() WHERE tenant_id = @tenant_id AND id = @id AND status IN ('open', 'acknowledged')";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> CloseAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.incidents SET status = 'closed', closed_at = NOW() WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> AssignAsync(string tenantId, Guid id, Guid assignedTo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "UPDATE notify.incidents SET assigned_to = @assigned_to WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "id", id);
|
||||
AddParameter(cmd, "assigned_to", assignedTo);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static IncidentEntity MapIncident(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Title = reader.GetString(2),
|
||||
Description = GetNullableString(reader, 3),
|
||||
Severity = reader.GetString(4),
|
||||
Status = reader.GetString(5),
|
||||
Source = GetNullableString(reader, 6),
|
||||
CorrelationId = GetNullableString(reader, 7),
|
||||
AssignedTo = GetNullableGuid(reader, 8),
|
||||
EscalationPolicyId = GetNullableGuid(reader, 9),
|
||||
Metadata = reader.GetString(10),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11),
|
||||
AcknowledgedAt = GetNullableDateTimeOffset(reader, 12),
|
||||
ResolvedAt = GetNullableDateTimeOffset(reader, 13),
|
||||
ClosedAt = GetNullableDateTimeOffset(reader, 14),
|
||||
CreatedBy = GetNullableString(reader, 15)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class MaintenanceWindowRepository : RepositoryBase<NotifyDataSource>, IMaintenanceWindowRepository
|
||||
{
|
||||
public MaintenanceWindowRepository(NotifyDataSource dataSource, ILogger<MaintenanceWindowRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<MaintenanceWindowEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, start_at, end_at, suppress_channels, suppress_event_types, created_at, created_by
|
||||
FROM notify.maintenance_windows WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapWindow, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<MaintenanceWindowEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, start_at, end_at, suppress_channels, suppress_event_types, created_at, created_by
|
||||
FROM notify.maintenance_windows WHERE tenant_id = @tenant_id ORDER BY start_at DESC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapWindow, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<MaintenanceWindowEntity>> GetActiveAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, start_at, end_at, suppress_channels, suppress_event_types, created_at, created_by
|
||||
FROM notify.maintenance_windows WHERE tenant_id = @tenant_id AND start_at <= NOW() AND end_at > NOW()
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapWindow, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<MaintenanceWindowEntity> CreateAsync(MaintenanceWindowEntity window, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.maintenance_windows (id, tenant_id, name, description, start_at, end_at, suppress_channels, suppress_event_types, created_by)
|
||||
VALUES (@id, @tenant_id, @name, @description, @start_at, @end_at, @suppress_channels, @suppress_event_types, @created_by)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = window.Id == Guid.Empty ? Guid.NewGuid() : window.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(window.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", window.TenantId);
|
||||
AddParameter(command, "name", window.Name);
|
||||
AddParameter(command, "description", window.Description);
|
||||
AddParameter(command, "start_at", window.StartAt);
|
||||
AddParameter(command, "end_at", window.EndAt);
|
||||
AddParameter(command, "suppress_channels", window.SuppressChannels);
|
||||
AddTextArrayParameter(command, "suppress_event_types", window.SuppressEventTypes ?? []);
|
||||
AddParameter(command, "created_by", window.CreatedBy);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapWindow(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(MaintenanceWindowEntity window, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.maintenance_windows SET name = @name, description = @description, start_at = @start_at, end_at = @end_at,
|
||||
suppress_channels = @suppress_channels, suppress_event_types = @suppress_event_types
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
var rows = await ExecuteAsync(window.TenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", window.TenantId);
|
||||
AddParameter(cmd, "id", window.Id);
|
||||
AddParameter(cmd, "name", window.Name);
|
||||
AddParameter(cmd, "description", window.Description);
|
||||
AddParameter(cmd, "start_at", window.StartAt);
|
||||
AddParameter(cmd, "end_at", window.EndAt);
|
||||
AddParameter(cmd, "suppress_channels", window.SuppressChannels);
|
||||
AddTextArrayParameter(cmd, "suppress_event_types", window.SuppressEventTypes ?? []);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.maintenance_windows WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteExpiredAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.maintenance_windows WHERE end_at < @cutoff";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "cutoff", cutoff);
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static MaintenanceWindowEntity MapWindow(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
Description = GetNullableString(reader, 3),
|
||||
StartAt = reader.GetFieldValue<DateTimeOffset>(4),
|
||||
EndAt = reader.GetFieldValue<DateTimeOffset>(5),
|
||||
SuppressChannels = reader.IsDBNull(6) ? null : reader.GetFieldValue<Guid[]>(6),
|
||||
SuppressEventTypes = reader.IsDBNull(7) ? null : reader.GetFieldValue<string[]>(7),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(8),
|
||||
CreatedBy = GetNullableString(reader, 9)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class NotifyAuditRepository : RepositoryBase<NotifyDataSource>, INotifyAuditRepository
|
||||
{
|
||||
public NotifyAuditRepository(NotifyDataSource dataSource, ILogger<NotifyAuditRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<long> CreateAsync(NotifyAuditEntity audit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.audit (tenant_id, user_id, action, resource_type, resource_id, details, correlation_id)
|
||||
VALUES (@tenant_id, @user_id, @action, @resource_type, @resource_id, @details::jsonb, @correlation_id)
|
||||
RETURNING id
|
||||
""";
|
||||
await using var connection = await DataSource.OpenConnectionAsync(audit.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "tenant_id", audit.TenantId);
|
||||
AddParameter(command, "user_id", audit.UserId);
|
||||
AddParameter(command, "action", audit.Action);
|
||||
AddParameter(command, "resource_type", audit.ResourceType);
|
||||
AddParameter(command, "resource_id", audit.ResourceId);
|
||||
AddJsonbParameter(command, "details", audit.Details);
|
||||
AddParameter(command, "correlation_id", audit.CorrelationId);
|
||||
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return (long)result!;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NotifyAuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, details, correlation_id, created_at
|
||||
FROM notify.audit WHERE tenant_id = @tenant_id
|
||||
ORDER BY created_at DESC LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NotifyAuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId = null, int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, details, correlation_id, created_at
|
||||
FROM notify.audit WHERE tenant_id = @tenant_id AND resource_type = @resource_type
|
||||
""";
|
||||
if (resourceId != null) sql += " AND resource_id = @resource_id";
|
||||
sql += " ORDER BY created_at DESC LIMIT @limit";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "resource_type", resourceType);
|
||||
if (resourceId != null) AddParameter(cmd, "resource_id", resourceId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NotifyAuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, details, correlation_id, created_at
|
||||
FROM notify.audit WHERE tenant_id = @tenant_id AND correlation_id = @correlation_id
|
||||
ORDER BY created_at
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "correlation_id", correlationId); },
|
||||
MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.audit WHERE created_at < @cutoff";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "cutoff", cutoff);
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static NotifyAuditEntity MapAudit(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetInt64(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = GetNullableGuid(reader, 2),
|
||||
Action = reader.GetString(3),
|
||||
ResourceType = reader.GetString(4),
|
||||
ResourceId = GetNullableString(reader, 5),
|
||||
Details = GetNullableString(reader, 6),
|
||||
CorrelationId = GetNullableString(reader, 7),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(8)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class OnCallScheduleRepository : RepositoryBase<NotifyDataSource>, IOnCallScheduleRepository
|
||||
{
|
||||
public OnCallScheduleRepository(NotifyDataSource dataSource, ILogger<OnCallScheduleRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<OnCallScheduleEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, timezone, rotation_type, participants, overrides, metadata, created_at, updated_at
|
||||
FROM notify.on_call_schedules WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapSchedule, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<OnCallScheduleEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, timezone, rotation_type, participants, overrides, metadata, created_at, updated_at
|
||||
FROM notify.on_call_schedules WHERE tenant_id = @tenant_id AND name = @name
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "name", name); },
|
||||
MapSchedule, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<OnCallScheduleEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, timezone, rotation_type, participants, overrides, metadata, created_at, updated_at
|
||||
FROM notify.on_call_schedules WHERE tenant_id = @tenant_id ORDER BY name
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapSchedule, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<OnCallScheduleEntity> CreateAsync(OnCallScheduleEntity schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.on_call_schedules (id, tenant_id, name, description, timezone, rotation_type, participants, overrides, metadata)
|
||||
VALUES (@id, @tenant_id, @name, @description, @timezone, @rotation_type, @participants::jsonb, @overrides::jsonb, @metadata::jsonb)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = schedule.Id == Guid.Empty ? Guid.NewGuid() : schedule.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(schedule.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", schedule.TenantId);
|
||||
AddParameter(command, "name", schedule.Name);
|
||||
AddParameter(command, "description", schedule.Description);
|
||||
AddParameter(command, "timezone", schedule.Timezone);
|
||||
AddParameter(command, "rotation_type", schedule.RotationType);
|
||||
AddJsonbParameter(command, "participants", schedule.Participants);
|
||||
AddJsonbParameter(command, "overrides", schedule.Overrides);
|
||||
AddJsonbParameter(command, "metadata", schedule.Metadata);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapSchedule(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(OnCallScheduleEntity schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.on_call_schedules SET name = @name, description = @description, timezone = @timezone,
|
||||
rotation_type = @rotation_type, participants = @participants::jsonb, overrides = @overrides::jsonb, metadata = @metadata::jsonb
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
var rows = await ExecuteAsync(schedule.TenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", schedule.TenantId);
|
||||
AddParameter(cmd, "id", schedule.Id);
|
||||
AddParameter(cmd, "name", schedule.Name);
|
||||
AddParameter(cmd, "description", schedule.Description);
|
||||
AddParameter(cmd, "timezone", schedule.Timezone);
|
||||
AddParameter(cmd, "rotation_type", schedule.RotationType);
|
||||
AddJsonbParameter(cmd, "participants", schedule.Participants);
|
||||
AddJsonbParameter(cmd, "overrides", schedule.Overrides);
|
||||
AddJsonbParameter(cmd, "metadata", schedule.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.on_call_schedules WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static OnCallScheduleEntity MapSchedule(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
Description = GetNullableString(reader, 3),
|
||||
Timezone = reader.GetString(4),
|
||||
RotationType = reader.GetString(5),
|
||||
Participants = reader.GetString(6),
|
||||
Overrides = reader.GetString(7),
|
||||
Metadata = reader.GetString(8),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(9),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(10)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class QuietHoursRepository : RepositoryBase<NotifyDataSource>, IQuietHoursRepository
|
||||
{
|
||||
public QuietHoursRepository(NotifyDataSource dataSource, ILogger<QuietHoursRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<QuietHoursEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, channel_id, start_time, end_time, timezone, days_of_week, enabled, created_at, updated_at
|
||||
FROM notify.quiet_hours WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapQuietHours, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<QuietHoursEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, channel_id, start_time, end_time, timezone, days_of_week, enabled, created_at, updated_at
|
||||
FROM notify.quiet_hours WHERE tenant_id = @tenant_id ORDER BY start_time
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => AddParameter(cmd, "tenant_id", tenantId),
|
||||
MapQuietHours, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<QuietHoursEntity>> GetForUserAsync(string tenantId, Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, channel_id, start_time, end_time, timezone, days_of_week, enabled, created_at, updated_at
|
||||
FROM notify.quiet_hours WHERE tenant_id = @tenant_id AND (user_id IS NULL OR user_id = @user_id) AND enabled = TRUE
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "user_id", userId); },
|
||||
MapQuietHours, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<QuietHoursEntity> CreateAsync(QuietHoursEntity quietHours, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.quiet_hours (id, tenant_id, user_id, channel_id, start_time, end_time, timezone, days_of_week, enabled)
|
||||
VALUES (@id, @tenant_id, @user_id, @channel_id, @start_time, @end_time, @timezone, @days_of_week, @enabled)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = quietHours.Id == Guid.Empty ? Guid.NewGuid() : quietHours.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(quietHours.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", quietHours.TenantId);
|
||||
AddParameter(command, "user_id", quietHours.UserId);
|
||||
AddParameter(command, "channel_id", quietHours.ChannelId);
|
||||
AddParameter(command, "start_time", quietHours.StartTime);
|
||||
AddParameter(command, "end_time", quietHours.EndTime);
|
||||
AddParameter(command, "timezone", quietHours.Timezone);
|
||||
AddParameter(command, "days_of_week", quietHours.DaysOfWeek);
|
||||
AddParameter(command, "enabled", quietHours.Enabled);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapQuietHours(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(QuietHoursEntity quietHours, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.quiet_hours SET user_id = @user_id, channel_id = @channel_id, start_time = @start_time, end_time = @end_time,
|
||||
timezone = @timezone, days_of_week = @days_of_week, enabled = @enabled
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
var rows = await ExecuteAsync(quietHours.TenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", quietHours.TenantId);
|
||||
AddParameter(cmd, "id", quietHours.Id);
|
||||
AddParameter(cmd, "user_id", quietHours.UserId);
|
||||
AddParameter(cmd, "channel_id", quietHours.ChannelId);
|
||||
AddParameter(cmd, "start_time", quietHours.StartTime);
|
||||
AddParameter(cmd, "end_time", quietHours.EndTime);
|
||||
AddParameter(cmd, "timezone", quietHours.Timezone);
|
||||
AddParameter(cmd, "days_of_week", quietHours.DaysOfWeek);
|
||||
AddParameter(cmd, "enabled", quietHours.Enabled);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.quiet_hours WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static QuietHoursEntity MapQuietHours(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = GetNullableGuid(reader, 2),
|
||||
ChannelId = GetNullableGuid(reader, 3),
|
||||
StartTime = reader.GetFieldValue<TimeOnly>(4),
|
||||
EndTime = reader.GetFieldValue<TimeOnly>(5),
|
||||
Timezone = reader.GetString(6),
|
||||
DaysOfWeek = reader.IsDBNull(7) ? [0, 1, 2, 3, 4, 5, 6] : reader.GetFieldValue<int[]>(7),
|
||||
Enabled = reader.GetBoolean(8),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(9),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(10)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class RuleRepository : RepositoryBase<NotifyDataSource>, IRuleRepository
|
||||
{
|
||||
public RuleRepository(NotifyDataSource dataSource, ILogger<RuleRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<RuleEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, enabled, priority, event_types, filter, channel_ids, template_id, metadata, created_at, updated_at
|
||||
FROM notify.rules WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapRule, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<RuleEntity?> GetByNameAsync(string tenantId, string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, enabled, priority, event_types, filter, channel_ids, template_id, metadata, created_at, updated_at
|
||||
FROM notify.rules WHERE tenant_id = @tenant_id AND name = @name
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "name", name); },
|
||||
MapRule, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RuleEntity>> ListAsync(string tenantId, bool? enabled = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant_id, name, description, enabled, priority, event_types, filter, channel_ids, template_id, metadata, created_at, updated_at
|
||||
FROM notify.rules WHERE tenant_id = @tenant_id
|
||||
""";
|
||||
if (enabled.HasValue) sql += " AND enabled = @enabled";
|
||||
sql += " ORDER BY priority DESC, name";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
if (enabled.HasValue) AddParameter(cmd, "enabled", enabled.Value);
|
||||
}, MapRule, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RuleEntity>> GetMatchingRulesAsync(string tenantId, string eventType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, description, enabled, priority, event_types, filter, channel_ids, template_id, metadata, created_at, updated_at
|
||||
FROM notify.rules WHERE tenant_id = @tenant_id AND enabled = TRUE AND @event_type = ANY(event_types)
|
||||
ORDER BY priority DESC
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "event_type", eventType); },
|
||||
MapRule, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<RuleEntity> CreateAsync(RuleEntity rule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.rules (id, tenant_id, name, description, enabled, priority, event_types, filter, channel_ids, template_id, metadata)
|
||||
VALUES (@id, @tenant_id, @name, @description, @enabled, @priority, @event_types, @filter::jsonb, @channel_ids, @template_id, @metadata::jsonb)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = rule.Id == Guid.Empty ? Guid.NewGuid() : rule.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(rule.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", rule.TenantId);
|
||||
AddParameter(command, "name", rule.Name);
|
||||
AddParameter(command, "description", rule.Description);
|
||||
AddParameter(command, "enabled", rule.Enabled);
|
||||
AddParameter(command, "priority", rule.Priority);
|
||||
AddTextArrayParameter(command, "event_types", rule.EventTypes);
|
||||
AddJsonbParameter(command, "filter", rule.Filter);
|
||||
AddParameter(command, "channel_ids", rule.ChannelIds);
|
||||
AddParameter(command, "template_id", rule.TemplateId);
|
||||
AddJsonbParameter(command, "metadata", rule.Metadata);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapRule(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(RuleEntity rule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.rules SET name = @name, description = @description, enabled = @enabled, priority = @priority,
|
||||
event_types = @event_types, filter = @filter::jsonb, channel_ids = @channel_ids, template_id = @template_id, metadata = @metadata::jsonb
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
var rows = await ExecuteAsync(rule.TenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", rule.TenantId);
|
||||
AddParameter(cmd, "id", rule.Id);
|
||||
AddParameter(cmd, "name", rule.Name);
|
||||
AddParameter(cmd, "description", rule.Description);
|
||||
AddParameter(cmd, "enabled", rule.Enabled);
|
||||
AddParameter(cmd, "priority", rule.Priority);
|
||||
AddTextArrayParameter(cmd, "event_types", rule.EventTypes);
|
||||
AddJsonbParameter(cmd, "filter", rule.Filter);
|
||||
AddParameter(cmd, "channel_ids", rule.ChannelIds);
|
||||
AddParameter(cmd, "template_id", rule.TemplateId);
|
||||
AddJsonbParameter(cmd, "metadata", rule.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.rules WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static RuleEntity MapRule(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
Description = GetNullableString(reader, 3),
|
||||
Enabled = reader.GetBoolean(4),
|
||||
Priority = reader.GetInt32(5),
|
||||
EventTypes = reader.IsDBNull(6) ? [] : reader.GetFieldValue<string[]>(6),
|
||||
Filter = reader.GetString(7),
|
||||
ChannelIds = reader.IsDBNull(8) ? [] : reader.GetFieldValue<Guid[]>(8),
|
||||
TemplateId = GetNullableGuid(reader, 9),
|
||||
Metadata = reader.GetString(10),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(12)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Notify.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Notify.Storage.Postgres.Repositories;
|
||||
|
||||
public sealed class TemplateRepository : RepositoryBase<NotifyDataSource>, ITemplateRepository
|
||||
{
|
||||
public TemplateRepository(NotifyDataSource dataSource, ILogger<TemplateRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<TemplateEntity?> GetByIdAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, channel_type::text, subject_template, body_template, locale, metadata, created_at, updated_at
|
||||
FROM notify.templates WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
MapTemplate, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TemplateEntity?> GetByNameAsync(string tenantId, string name, ChannelType channelType, string locale = "en", CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, name, channel_type::text, subject_template, body_template, locale, metadata, created_at, updated_at
|
||||
FROM notify.templates WHERE tenant_id = @tenant_id AND name = @name AND channel_type = @channel_type::notify.channel_type AND locale = @locale
|
||||
""";
|
||||
return await QuerySingleOrDefaultAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "name", name);
|
||||
AddParameter(cmd, "channel_type", ChannelTypeToString(channelType));
|
||||
AddParameter(cmd, "locale", locale);
|
||||
}, MapTemplate, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TemplateEntity>> ListAsync(string tenantId, ChannelType? channelType = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant_id, name, channel_type::text, subject_template, body_template, locale, metadata, created_at, updated_at
|
||||
FROM notify.templates WHERE tenant_id = @tenant_id
|
||||
""";
|
||||
if (channelType.HasValue) sql += " AND channel_type = @channel_type::notify.channel_type";
|
||||
sql += " ORDER BY name, locale";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
if (channelType.HasValue) AddParameter(cmd, "channel_type", ChannelTypeToString(channelType.Value));
|
||||
}, MapTemplate, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TemplateEntity> CreateAsync(TemplateEntity template, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO notify.templates (id, tenant_id, name, channel_type, subject_template, body_template, locale, metadata)
|
||||
VALUES (@id, @tenant_id, @name, @channel_type::notify.channel_type, @subject_template, @body_template, @locale, @metadata::jsonb)
|
||||
RETURNING id, tenant_id, name, channel_type::text, subject_template, body_template, locale, metadata, created_at, updated_at
|
||||
""";
|
||||
var id = template.Id == Guid.Empty ? Guid.NewGuid() : template.Id;
|
||||
await using var connection = await DataSource.OpenConnectionAsync(template.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "tenant_id", template.TenantId);
|
||||
AddParameter(command, "name", template.Name);
|
||||
AddParameter(command, "channel_type", ChannelTypeToString(template.ChannelType));
|
||||
AddParameter(command, "subject_template", template.SubjectTemplate);
|
||||
AddParameter(command, "body_template", template.BodyTemplate);
|
||||
AddParameter(command, "locale", template.Locale);
|
||||
AddJsonbParameter(command, "metadata", template.Metadata);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapTemplate(reader);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(TemplateEntity template, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE notify.templates SET name = @name, channel_type = @channel_type::notify.channel_type,
|
||||
subject_template = @subject_template, body_template = @body_template, locale = @locale, metadata = @metadata::jsonb
|
||||
WHERE tenant_id = @tenant_id AND id = @id
|
||||
""";
|
||||
var rows = await ExecuteAsync(template.TenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", template.TenantId);
|
||||
AddParameter(cmd, "id", template.Id);
|
||||
AddParameter(cmd, "name", template.Name);
|
||||
AddParameter(cmd, "channel_type", ChannelTypeToString(template.ChannelType));
|
||||
AddParameter(cmd, "subject_template", template.SubjectTemplate);
|
||||
AddParameter(cmd, "body_template", template.BodyTemplate);
|
||||
AddParameter(cmd, "locale", template.Locale);
|
||||
AddJsonbParameter(cmd, "metadata", template.Metadata);
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string tenantId, Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM notify.templates WHERE tenant_id = @tenant_id AND id = @id";
|
||||
var rows = await ExecuteAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "id", id); },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static TemplateEntity MapTemplate(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
Name = reader.GetString(2),
|
||||
ChannelType = ParseChannelType(reader.GetString(3)),
|
||||
SubjectTemplate = GetNullableString(reader, 4),
|
||||
BodyTemplate = reader.GetString(5),
|
||||
Locale = reader.GetString(6),
|
||||
Metadata = reader.GetString(7),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(8),
|
||||
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(9)
|
||||
};
|
||||
|
||||
private static string ChannelTypeToString(ChannelType t) => t switch
|
||||
{
|
||||
ChannelType.Email => "email", ChannelType.Slack => "slack", ChannelType.Teams => "teams",
|
||||
ChannelType.Webhook => "webhook", ChannelType.PagerDuty => "pagerduty", ChannelType.OpsGenie => "opsgenie",
|
||||
_ => throw new ArgumentException($"Unknown: {t}")
|
||||
};
|
||||
|
||||
private static ChannelType ParseChannelType(string s) => s switch
|
||||
{
|
||||
"email" => ChannelType.Email, "slack" => ChannelType.Slack, "teams" => ChannelType.Teams,
|
||||
"webhook" => ChannelType.Webhook, "pagerduty" => ChannelType.PagerDuty, "opsgenie" => ChannelType.OpsGenie,
|
||||
_ => throw new ArgumentException($"Unknown: {s}")
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user