up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-14 15:50:38 +02:00
parent f1a39c4ce3
commit 233873f620
249 changed files with 29746 additions and 154 deletions

View File

@@ -0,0 +1,49 @@
using StellaOps.Notify.Storage.Postgres.Models;
namespace StellaOps.Notify.Storage.Postgres.Repositories;
/// <summary>
/// Repository interface for localization bundles.
/// </summary>
public interface ILocalizationBundleRepository
{
/// <summary>
/// Gets a localization bundle by ID.
/// </summary>
Task<LocalizationBundleEntity?> GetByIdAsync(string tenantId, string bundleId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all localization bundles for a tenant.
/// </summary>
Task<IReadOnlyList<LocalizationBundleEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets localization bundles by bundle key.
/// </summary>
Task<IReadOnlyList<LocalizationBundleEntity>> GetByBundleKeyAsync(string tenantId, string bundleKey, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a specific localization bundle by key and locale.
/// </summary>
Task<LocalizationBundleEntity?> GetByKeyAndLocaleAsync(string tenantId, string bundleKey, string locale, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the default bundle for a key.
/// </summary>
Task<LocalizationBundleEntity?> GetDefaultByKeyAsync(string tenantId, string bundleKey, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new localization bundle.
/// </summary>
Task<LocalizationBundleEntity> CreateAsync(LocalizationBundleEntity bundle, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing localization bundle.
/// </summary>
Task<bool> UpdateAsync(LocalizationBundleEntity bundle, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a localization bundle.
/// </summary>
Task<bool> DeleteAsync(string tenantId, string bundleId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,44 @@
using StellaOps.Notify.Storage.Postgres.Models;
namespace StellaOps.Notify.Storage.Postgres.Repositories;
/// <summary>
/// Repository interface for operator overrides.
/// </summary>
public interface IOperatorOverrideRepository
{
/// <summary>
/// Gets an operator override by ID.
/// </summary>
Task<OperatorOverrideEntity?> GetByIdAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all operator overrides for a tenant.
/// </summary>
Task<IReadOnlyList<OperatorOverrideEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets active (non-expired) operator overrides for a tenant.
/// </summary>
Task<IReadOnlyList<OperatorOverrideEntity>> GetActiveAsync(string tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets active overrides by type.
/// </summary>
Task<IReadOnlyList<OperatorOverrideEntity>> GetActiveByTypeAsync(string tenantId, string overrideType, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new operator override.
/// </summary>
Task<OperatorOverrideEntity> CreateAsync(OperatorOverrideEntity override_, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes an operator override.
/// </summary>
Task<bool> DeleteAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes all expired overrides for a tenant.
/// </summary>
Task<int> DeleteExpiredAsync(string tenantId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,216 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Notify.Storage.Postgres.Models;
namespace StellaOps.Notify.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="ILocalizationBundleRepository"/>.
/// </summary>
public sealed class LocalizationBundleRepository : RepositoryBase<NotifyDataSource>, ILocalizationBundleRepository
{
private bool _tableInitialized;
public LocalizationBundleRepository(NotifyDataSource dataSource, ILogger<LocalizationBundleRepository> logger)
: base(dataSource, logger) { }
public async Task<LocalizationBundleEntity?> GetByIdAsync(string tenantId, string bundleId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT bundle_id, tenant_id, locale, bundle_key, strings, is_default, parent_locale,
description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.localization_bundles WHERE tenant_id = @tenant_id AND bundle_id = @bundle_id
""";
return await QuerySingleOrDefaultAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "bundle_id", bundleId); },
MapLocalizationBundle, cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<LocalizationBundleEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT bundle_id, tenant_id, locale, bundle_key, strings, is_default, parent_locale,
description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.localization_bundles WHERE tenant_id = @tenant_id ORDER BY bundle_key, locale
""";
return await QueryAsync(tenantId, sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
MapLocalizationBundle, cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<LocalizationBundleEntity>> GetByBundleKeyAsync(string tenantId, string bundleKey, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT bundle_id, tenant_id, locale, bundle_key, strings, is_default, parent_locale,
description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.localization_bundles WHERE tenant_id = @tenant_id AND bundle_key = @bundle_key ORDER BY locale
""";
return await QueryAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "bundle_key", bundleKey); },
MapLocalizationBundle, cancellationToken).ConfigureAwait(false);
}
public async Task<LocalizationBundleEntity?> GetByKeyAndLocaleAsync(string tenantId, string bundleKey, string locale, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT bundle_id, tenant_id, locale, bundle_key, strings, is_default, parent_locale,
description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.localization_bundles
WHERE tenant_id = @tenant_id AND bundle_key = @bundle_key AND LOWER(locale) = LOWER(@locale)
LIMIT 1
""";
return await QuerySingleOrDefaultAsync(tenantId, sql,
cmd =>
{
AddParameter(cmd, "tenant_id", tenantId);
AddParameter(cmd, "bundle_key", bundleKey);
AddParameter(cmd, "locale", locale);
},
MapLocalizationBundle, cancellationToken).ConfigureAwait(false);
}
public async Task<LocalizationBundleEntity?> GetDefaultByKeyAsync(string tenantId, string bundleKey, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT bundle_id, tenant_id, locale, bundle_key, strings, is_default, parent_locale,
description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.localization_bundles
WHERE tenant_id = @tenant_id AND bundle_key = @bundle_key AND is_default = TRUE
LIMIT 1
""";
return await QuerySingleOrDefaultAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "bundle_key", bundleKey); },
MapLocalizationBundle, cancellationToken).ConfigureAwait(false);
}
public async Task<LocalizationBundleEntity> CreateAsync(LocalizationBundleEntity bundle, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(bundle.TenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
INSERT INTO notify.localization_bundles (bundle_id, tenant_id, locale, bundle_key, strings, is_default,
parent_locale, description, metadata, created_by, updated_by)
VALUES (@bundle_id, @tenant_id, @locale, @bundle_key, @strings, @is_default,
@parent_locale, @description, @metadata, @created_by, @updated_by)
RETURNING bundle_id, tenant_id, locale, bundle_key, strings, is_default, parent_locale,
description, metadata, created_by, created_at, updated_by, updated_at
""";
await using var connection = await DataSource.OpenConnectionAsync(bundle.TenantId, "writer", cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "bundle_id", bundle.BundleId);
AddParameter(command, "tenant_id", bundle.TenantId);
AddParameter(command, "locale", bundle.Locale);
AddParameter(command, "bundle_key", bundle.BundleKey);
AddJsonbParameter(command, "strings", bundle.Strings);
AddParameter(command, "is_default", bundle.IsDefault);
AddParameter(command, "parent_locale", (object?)bundle.ParentLocale ?? DBNull.Value);
AddParameter(command, "description", (object?)bundle.Description ?? DBNull.Value);
AddJsonbParameter(command, "metadata", bundle.Metadata);
AddParameter(command, "created_by", (object?)bundle.CreatedBy ?? DBNull.Value);
AddParameter(command, "updated_by", (object?)bundle.UpdatedBy ?? DBNull.Value);
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
return MapLocalizationBundle(reader);
}
public async Task<bool> UpdateAsync(LocalizationBundleEntity bundle, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(bundle.TenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
UPDATE notify.localization_bundles
SET locale = @locale, bundle_key = @bundle_key, strings = @strings, is_default = @is_default,
parent_locale = @parent_locale, description = @description, metadata = @metadata, updated_by = @updated_by
WHERE tenant_id = @tenant_id AND bundle_id = @bundle_id
""";
var rows = await ExecuteAsync(bundle.TenantId, sql, cmd =>
{
AddParameter(cmd, "tenant_id", bundle.TenantId);
AddParameter(cmd, "bundle_id", bundle.BundleId);
AddParameter(cmd, "locale", bundle.Locale);
AddParameter(cmd, "bundle_key", bundle.BundleKey);
AddJsonbParameter(cmd, "strings", bundle.Strings);
AddParameter(cmd, "is_default", bundle.IsDefault);
AddParameter(cmd, "parent_locale", (object?)bundle.ParentLocale ?? DBNull.Value);
AddParameter(cmd, "description", (object?)bundle.Description ?? DBNull.Value);
AddJsonbParameter(cmd, "metadata", bundle.Metadata);
AddParameter(cmd, "updated_by", (object?)bundle.UpdatedBy ?? DBNull.Value);
}, cancellationToken).ConfigureAwait(false);
return rows > 0;
}
public async Task<bool> DeleteAsync(string tenantId, string bundleId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = "DELETE FROM notify.localization_bundles WHERE tenant_id = @tenant_id AND bundle_id = @bundle_id";
var rows = await ExecuteAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "bundle_id", bundleId); },
cancellationToken).ConfigureAwait(false);
return rows > 0;
}
private static LocalizationBundleEntity MapLocalizationBundle(NpgsqlDataReader reader) => new()
{
BundleId = reader.GetString(0),
TenantId = reader.GetString(1),
Locale = reader.GetString(2),
BundleKey = reader.GetString(3),
Strings = reader.GetString(4),
IsDefault = reader.GetBoolean(5),
ParentLocale = GetNullableString(reader, 6),
Description = GetNullableString(reader, 7),
Metadata = GetNullableString(reader, 8),
CreatedBy = GetNullableString(reader, 9),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(10),
UpdatedBy = GetNullableString(reader, 11),
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(12)
};
private async Task EnsureTableAsync(string tenantId, CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = """
CREATE TABLE IF NOT EXISTS notify.localization_bundles (
bundle_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
locale TEXT NOT NULL,
bundle_key TEXT NOT NULL,
strings JSONB NOT NULL,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
parent_locale TEXT,
description TEXT,
metadata JSONB,
created_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id, bundle_id)
);
CREATE INDEX IF NOT EXISTS idx_localization_bundles_key ON notify.localization_bundles (tenant_id, bundle_key);
CREATE UNIQUE INDEX IF NOT EXISTS idx_localization_bundles_key_locale ON notify.localization_bundles (tenant_id, bundle_key, locale);
CREATE INDEX IF NOT EXISTS idx_localization_bundles_default ON notify.localization_bundles (tenant_id, bundle_key, is_default) WHERE is_default = TRUE;
""";
await ExecuteAsync(tenantId, ddl, _ => { }, cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,160 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Notify.Storage.Postgres.Models;
namespace StellaOps.Notify.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="IOperatorOverrideRepository"/>.
/// </summary>
public sealed class OperatorOverrideRepository : RepositoryBase<NotifyDataSource>, IOperatorOverrideRepository
{
private bool _tableInitialized;
public OperatorOverrideRepository(NotifyDataSource dataSource, ILogger<OperatorOverrideRepository> logger)
: base(dataSource, logger) { }
public async Task<OperatorOverrideEntity?> GetByIdAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT override_id, tenant_id, override_type, expires_at, channel_id, rule_id, reason, created_by, created_at
FROM notify.operator_overrides WHERE tenant_id = @tenant_id AND override_id = @override_id
""";
return await QuerySingleOrDefaultAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "override_id", overrideId); },
MapOperatorOverride, cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<OperatorOverrideEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT override_id, tenant_id, override_type, expires_at, channel_id, rule_id, reason, created_by, created_at
FROM notify.operator_overrides WHERE tenant_id = @tenant_id ORDER BY created_at DESC
""";
return await QueryAsync(tenantId, sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
MapOperatorOverride, cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<OperatorOverrideEntity>> GetActiveAsync(string tenantId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT override_id, tenant_id, override_type, expires_at, channel_id, rule_id, reason, created_by, created_at
FROM notify.operator_overrides WHERE tenant_id = @tenant_id AND expires_at > NOW() ORDER BY created_at DESC
""";
return await QueryAsync(tenantId, sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
MapOperatorOverride, cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<OperatorOverrideEntity>> GetActiveByTypeAsync(string tenantId, string overrideType, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT override_id, tenant_id, override_type, expires_at, channel_id, rule_id, reason, created_by, created_at
FROM notify.operator_overrides WHERE tenant_id = @tenant_id AND override_type = @override_type AND expires_at > NOW()
ORDER BY created_at DESC
""";
return await QueryAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "override_type", overrideType); },
MapOperatorOverride, cancellationToken).ConfigureAwait(false);
}
public async Task<OperatorOverrideEntity> CreateAsync(OperatorOverrideEntity override_, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(override_.TenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
INSERT INTO notify.operator_overrides (override_id, tenant_id, override_type, expires_at, channel_id, rule_id, reason, created_by)
VALUES (@override_id, @tenant_id, @override_type, @expires_at, @channel_id, @rule_id, @reason, @created_by)
RETURNING override_id, tenant_id, override_type, expires_at, channel_id, rule_id, reason, created_by, created_at
""";
await using var connection = await DataSource.OpenConnectionAsync(override_.TenantId, "writer", cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "override_id", override_.OverrideId);
AddParameter(command, "tenant_id", override_.TenantId);
AddParameter(command, "override_type", override_.OverrideType);
AddParameter(command, "expires_at", override_.ExpiresAt);
AddParameter(command, "channel_id", (object?)override_.ChannelId ?? DBNull.Value);
AddParameter(command, "rule_id", (object?)override_.RuleId ?? DBNull.Value);
AddParameter(command, "reason", (object?)override_.Reason ?? DBNull.Value);
AddParameter(command, "created_by", (object?)override_.CreatedBy ?? DBNull.Value);
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
return MapOperatorOverride(reader);
}
public async Task<bool> DeleteAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = "DELETE FROM notify.operator_overrides WHERE tenant_id = @tenant_id AND override_id = @override_id";
var rows = await ExecuteAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "override_id", overrideId); },
cancellationToken).ConfigureAwait(false);
return rows > 0;
}
public async Task<int> DeleteExpiredAsync(string tenantId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = "DELETE FROM notify.operator_overrides WHERE tenant_id = @tenant_id AND expires_at <= NOW()";
return await ExecuteAsync(tenantId, sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
cancellationToken).ConfigureAwait(false);
}
private static OperatorOverrideEntity MapOperatorOverride(NpgsqlDataReader reader) => new()
{
OverrideId = reader.GetString(0),
TenantId = reader.GetString(1),
OverrideType = reader.GetString(2),
ExpiresAt = reader.GetFieldValue<DateTimeOffset>(3),
ChannelId = GetNullableString(reader, 4),
RuleId = GetNullableString(reader, 5),
Reason = GetNullableString(reader, 6),
CreatedBy = GetNullableString(reader, 7),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(8)
};
private async Task EnsureTableAsync(string tenantId, CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = """
CREATE TABLE IF NOT EXISTS notify.operator_overrides (
override_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
override_type TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
channel_id TEXT,
rule_id TEXT,
reason TEXT,
created_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id, override_id)
);
CREATE INDEX IF NOT EXISTS idx_operator_overrides_type ON notify.operator_overrides (tenant_id, override_type);
CREATE INDEX IF NOT EXISTS idx_operator_overrides_expires ON notify.operator_overrides (tenant_id, expires_at);
CREATE INDEX IF NOT EXISTS idx_operator_overrides_active ON notify.operator_overrides (tenant_id, override_type, expires_at) WHERE expires_at > NOW();
""";
await ExecuteAsync(tenantId, ddl, _ => { }, cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,198 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.Notify.Storage.Postgres.Models;
namespace StellaOps.Notify.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="IThrottleConfigRepository"/>.
/// </summary>
public sealed class ThrottleConfigRepository : RepositoryBase<NotifyDataSource>, IThrottleConfigRepository
{
private bool _tableInitialized;
public ThrottleConfigRepository(NotifyDataSource dataSource, ILogger<ThrottleConfigRepository> logger)
: base(dataSource, logger) { }
public async Task<ThrottleConfigEntity?> GetByIdAsync(string tenantId, string configId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT config_id, tenant_id, name, default_window_seconds, max_notifications_per_window, channel_id,
is_default, enabled, description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.throttle_configs WHERE tenant_id = @tenant_id AND config_id = @config_id
""";
return await QuerySingleOrDefaultAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "config_id", configId); },
MapThrottleConfig, cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<ThrottleConfigEntity>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT config_id, tenant_id, name, default_window_seconds, max_notifications_per_window, channel_id,
is_default, enabled, description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.throttle_configs WHERE tenant_id = @tenant_id ORDER BY name
""";
return await QueryAsync(tenantId, sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
MapThrottleConfig, cancellationToken).ConfigureAwait(false);
}
public async Task<ThrottleConfigEntity?> GetDefaultAsync(string tenantId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT config_id, tenant_id, name, default_window_seconds, max_notifications_per_window, channel_id,
is_default, enabled, description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.throttle_configs WHERE tenant_id = @tenant_id AND is_default = TRUE LIMIT 1
""";
return await QuerySingleOrDefaultAsync(tenantId, sql,
cmd => AddParameter(cmd, "tenant_id", tenantId),
MapThrottleConfig, cancellationToken).ConfigureAwait(false);
}
public async Task<ThrottleConfigEntity?> GetByChannelAsync(string tenantId, string channelId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
SELECT config_id, tenant_id, name, default_window_seconds, max_notifications_per_window, channel_id,
is_default, enabled, description, metadata, created_by, created_at, updated_by, updated_at
FROM notify.throttle_configs WHERE tenant_id = @tenant_id AND channel_id = @channel_id AND enabled = TRUE LIMIT 1
""";
return await QuerySingleOrDefaultAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "channel_id", channelId); },
MapThrottleConfig, cancellationToken).ConfigureAwait(false);
}
public async Task<ThrottleConfigEntity> CreateAsync(ThrottleConfigEntity config, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(config.TenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
INSERT INTO notify.throttle_configs (config_id, tenant_id, name, default_window_seconds, max_notifications_per_window,
channel_id, is_default, enabled, description, metadata, created_by, updated_by)
VALUES (@config_id, @tenant_id, @name, @default_window_seconds, @max_notifications_per_window,
@channel_id, @is_default, @enabled, @description, @metadata, @created_by, @updated_by)
RETURNING config_id, tenant_id, name, default_window_seconds, max_notifications_per_window, channel_id,
is_default, enabled, description, metadata, created_by, created_at, updated_by, updated_at
""";
await using var connection = await DataSource.OpenConnectionAsync(config.TenantId, "writer", cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "config_id", config.ConfigId);
AddParameter(command, "tenant_id", config.TenantId);
AddParameter(command, "name", config.Name);
AddParameter(command, "default_window_seconds", (long)config.DefaultWindow.TotalSeconds);
AddParameter(command, "max_notifications_per_window", (object?)config.MaxNotificationsPerWindow ?? DBNull.Value);
AddParameter(command, "channel_id", (object?)config.ChannelId ?? DBNull.Value);
AddParameter(command, "is_default", config.IsDefault);
AddParameter(command, "enabled", config.Enabled);
AddParameter(command, "description", (object?)config.Description ?? DBNull.Value);
AddJsonbParameter(command, "metadata", config.Metadata);
AddParameter(command, "created_by", (object?)config.CreatedBy ?? DBNull.Value);
AddParameter(command, "updated_by", (object?)config.UpdatedBy ?? DBNull.Value);
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
return MapThrottleConfig(reader);
}
public async Task<bool> UpdateAsync(ThrottleConfigEntity config, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(config.TenantId, cancellationToken).ConfigureAwait(false);
const string sql = """
UPDATE notify.throttle_configs
SET name = @name, default_window_seconds = @default_window_seconds,
max_notifications_per_window = @max_notifications_per_window, channel_id = @channel_id,
is_default = @is_default, enabled = @enabled, description = @description,
metadata = @metadata, updated_by = @updated_by
WHERE tenant_id = @tenant_id AND config_id = @config_id
""";
var rows = await ExecuteAsync(config.TenantId, sql, cmd =>
{
AddParameter(cmd, "tenant_id", config.TenantId);
AddParameter(cmd, "config_id", config.ConfigId);
AddParameter(cmd, "name", config.Name);
AddParameter(cmd, "default_window_seconds", (long)config.DefaultWindow.TotalSeconds);
AddParameter(cmd, "max_notifications_per_window", (object?)config.MaxNotificationsPerWindow ?? DBNull.Value);
AddParameter(cmd, "channel_id", (object?)config.ChannelId ?? DBNull.Value);
AddParameter(cmd, "is_default", config.IsDefault);
AddParameter(cmd, "enabled", config.Enabled);
AddParameter(cmd, "description", (object?)config.Description ?? DBNull.Value);
AddJsonbParameter(cmd, "metadata", config.Metadata);
AddParameter(cmd, "updated_by", (object?)config.UpdatedBy ?? DBNull.Value);
}, cancellationToken).ConfigureAwait(false);
return rows > 0;
}
public async Task<bool> DeleteAsync(string tenantId, string configId, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(tenantId, cancellationToken).ConfigureAwait(false);
const string sql = "DELETE FROM notify.throttle_configs WHERE tenant_id = @tenant_id AND config_id = @config_id";
var rows = await ExecuteAsync(tenantId, sql,
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "config_id", configId); },
cancellationToken).ConfigureAwait(false);
return rows > 0;
}
private static ThrottleConfigEntity MapThrottleConfig(NpgsqlDataReader reader) => new()
{
ConfigId = reader.GetString(0),
TenantId = reader.GetString(1),
Name = reader.GetString(2),
DefaultWindow = TimeSpan.FromSeconds(reader.GetInt64(3)),
MaxNotificationsPerWindow = GetNullableInt32(reader, 4),
ChannelId = GetNullableString(reader, 5),
IsDefault = reader.GetBoolean(6),
Enabled = reader.GetBoolean(7),
Description = GetNullableString(reader, 8),
Metadata = GetNullableString(reader, 9),
CreatedBy = GetNullableString(reader, 10),
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11),
UpdatedBy = GetNullableString(reader, 12),
UpdatedAt = reader.GetFieldValue<DateTimeOffset>(13)
};
private async Task EnsureTableAsync(string tenantId, CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = """
CREATE TABLE IF NOT EXISTS notify.throttle_configs (
config_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
default_window_seconds BIGINT NOT NULL DEFAULT 300,
max_notifications_per_window INTEGER,
channel_id TEXT,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
description TEXT,
metadata JSONB,
created_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id, config_id)
);
CREATE INDEX IF NOT EXISTS idx_throttle_configs_channel ON notify.throttle_configs (tenant_id, channel_id) WHERE channel_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_throttle_configs_default ON notify.throttle_configs (tenant_id, is_default) WHERE is_default = TRUE;
""";
await ExecuteAsync(tenantId, ddl, _ => { }, cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -42,6 +42,11 @@ public static class ServiceCollectionExtensions
services.AddScoped<INotifyAuditRepository, NotifyAuditRepository>();
services.AddScoped<ILockRepository, LockRepository>();
// Register new repositories (SPRINT-3412: PostgreSQL durability)
services.AddScoped<IThrottleConfigRepository, ThrottleConfigRepository>();
services.AddScoped<IOperatorOverrideRepository, OperatorOverrideRepository>();
services.AddScoped<ILocalizationBundleRepository, LocalizationBundleRepository>();
return services;
}
@@ -73,6 +78,11 @@ public static class ServiceCollectionExtensions
services.AddScoped<IIncidentRepository, IncidentRepository>();
services.AddScoped<INotifyAuditRepository, NotifyAuditRepository>();
// Register new repositories (SPRINT-3412: PostgreSQL durability)
services.AddScoped<IThrottleConfigRepository, ThrottleConfigRepository>();
services.AddScoped<IOperatorOverrideRepository, OperatorOverrideRepository>();
services.AddScoped<ILocalizationBundleRepository, LocalizationBundleRepository>();
return services;
}
}