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,26 @@
using System.Reflection;
using StellaOps.Infrastructure.Postgres.Testing;
using Xunit;
namespace StellaOps.PacksRegistry.Storage.Postgres.Tests;
/// <summary>
/// PostgreSQL integration test fixture for the PacksRegistry module.
/// </summary>
public sealed class PacksRegistryPostgresFixture : PostgresIntegrationFixture, ICollectionFixture<PacksRegistryPostgresFixture>
{
protected override Assembly? GetMigrationAssembly()
=> typeof(PacksRegistryDataSource).Assembly;
protected override string GetModuleName() => "PacksRegistry";
}
/// <summary>
/// Collection definition for PacksRegistry PostgreSQL integration tests.
/// Tests in this collection share a single PostgreSQL container instance.
/// </summary>
[CollectionDefinition(Name)]
public sealed class PacksRegistryPostgresCollection : ICollectionFixture<PacksRegistryPostgresFixture>
{
public const string Name = "PacksRegistryPostgres";
}

View File

@@ -0,0 +1,154 @@
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using MicrosoftOptions = Microsoft.Extensions.Options;
using StellaOps.PacksRegistry.Core.Models;
using StellaOps.PacksRegistry.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.PacksRegistry.Storage.Postgres.Tests;
[Collection(PacksRegistryPostgresCollection.Name)]
public sealed class PostgresPackRepositoryTests : IAsyncLifetime
{
private readonly PacksRegistryPostgresFixture _fixture;
private readonly PostgresPackRepository _repository;
private readonly string _tenantId = "tenant-" + Guid.NewGuid().ToString("N")[..8];
public PostgresPackRepositoryTests(PacksRegistryPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new PacksRegistryDataSource(MicrosoftOptions.Options.Create(options), NullLogger<PacksRegistryDataSource>.Instance);
_repository = new PostgresPackRepository(dataSource, NullLogger<PostgresPackRepository>.Instance);
}
public async Task InitializeAsync()
{
await _fixture.TruncateAllTablesAsync();
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task UpsertAndGet_RoundTripsPackRecord()
{
// Arrange
var packId = "pack-" + Guid.NewGuid().ToString("N");
var record = new PackRecord(
PackId: packId,
Name: "test-pack",
Version: "1.0.0",
TenantId: _tenantId,
Digest: "sha256:abc123",
Signature: "sig123",
ProvenanceUri: "https://example.com/provenance",
ProvenanceDigest: "sha256:prov456",
CreatedAtUtc: DateTimeOffset.UtcNow,
Metadata: new Dictionary<string, string> { ["author"] = "test" });
var content = Encoding.UTF8.GetBytes("pack content here");
var provenance = Encoding.UTF8.GetBytes("provenance data");
// Act
await _repository.UpsertAsync(record, content, provenance);
var fetched = await _repository.GetAsync(packId);
// Assert
fetched.Should().NotBeNull();
fetched!.PackId.Should().Be(packId);
fetched.Name.Should().Be("test-pack");
fetched.Version.Should().Be("1.0.0");
fetched.TenantId.Should().Be(_tenantId);
fetched.Metadata.Should().ContainKey("author");
}
[Fact]
public async Task GetContentAsync_ReturnsPackContent()
{
// Arrange
var packId = "pack-" + Guid.NewGuid().ToString("N");
var record = CreatePackRecord(packId, "content-test", "1.0.0");
var expectedContent = Encoding.UTF8.GetBytes("this is the pack content");
await _repository.UpsertAsync(record, expectedContent, null);
// Act
var content = await _repository.GetContentAsync(packId);
// Assert
content.Should().NotBeNull();
Encoding.UTF8.GetString(content!).Should().Be("this is the pack content");
}
[Fact]
public async Task GetProvenanceAsync_ReturnsProvenanceData()
{
// Arrange
var packId = "pack-" + Guid.NewGuid().ToString("N");
var record = CreatePackRecord(packId, "provenance-test", "1.0.0");
var content = Encoding.UTF8.GetBytes("content");
var expectedProvenance = Encoding.UTF8.GetBytes("provenance statement");
await _repository.UpsertAsync(record, content, expectedProvenance);
// Act
var provenance = await _repository.GetProvenanceAsync(packId);
// Assert
provenance.Should().NotBeNull();
Encoding.UTF8.GetString(provenance!).Should().Be("provenance statement");
}
[Fact]
public async Task ListAsync_ReturnsPacksForTenant()
{
// Arrange
var pack1 = CreatePackRecord("pack-1-" + Guid.NewGuid().ToString("N")[..8], "pack-a", "1.0.0");
var pack2 = CreatePackRecord("pack-2-" + Guid.NewGuid().ToString("N")[..8], "pack-b", "2.0.0");
var content = Encoding.UTF8.GetBytes("content");
await _repository.UpsertAsync(pack1, content, null);
await _repository.UpsertAsync(pack2, content, null);
// Act
var packs = await _repository.ListAsync(_tenantId);
// Assert
packs.Should().HaveCount(2);
}
[Fact]
public async Task UpsertAsync_UpdatesExistingPack()
{
// Arrange
var packId = "pack-" + Guid.NewGuid().ToString("N");
var record1 = CreatePackRecord(packId, "original", "1.0.0");
var record2 = CreatePackRecord(packId, "updated", "2.0.0");
var content = Encoding.UTF8.GetBytes("content");
// Act
await _repository.UpsertAsync(record1, content, null);
await _repository.UpsertAsync(record2, content, null);
var fetched = await _repository.GetAsync(packId);
// Assert
fetched.Should().NotBeNull();
fetched!.Name.Should().Be("updated");
fetched.Version.Should().Be("2.0.0");
}
private PackRecord CreatePackRecord(string packId, string name, string version) =>
new(
PackId: packId,
Name: name,
Version: version,
TenantId: _tenantId,
Digest: "sha256:" + Guid.NewGuid().ToString("N"),
Signature: null,
ProvenanceUri: null,
ProvenanceDigest: null,
CreatedAtUtc: DateTimeOffset.UtcNow,
Metadata: null);
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.PacksRegistry.Storage.Postgres\StellaOps.PacksRegistry.Storage.Postgres.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,44 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Connections;
using StellaOps.Infrastructure.Postgres.Options;
namespace StellaOps.PacksRegistry.Storage.Postgres;
/// <summary>
/// PostgreSQL data source for PacksRegistry module.
/// </summary>
public sealed class PacksRegistryDataSource : DataSourceBase
{
/// <summary>
/// Default schema name for PacksRegistry tables.
/// </summary>
public const string DefaultSchemaName = "packs";
/// <summary>
/// Creates a new PacksRegistry data source.
/// </summary>
public PacksRegistryDataSource(IOptions<PostgresOptions> options, ILogger<PacksRegistryDataSource> logger)
: base(CreateOptions(options.Value), logger)
{
}
/// <inheritdoc />
protected override string ModuleName => "PacksRegistry";
/// <inheritdoc />
protected override void ConfigureDataSourceBuilder(NpgsqlDataSourceBuilder builder)
{
base.ConfigureDataSourceBuilder(builder);
}
private static PostgresOptions CreateOptions(PostgresOptions baseOptions)
{
if (string.IsNullOrWhiteSpace(baseOptions.SchemaName))
{
baseOptions.SchemaName = DefaultSchemaName;
}
return baseOptions;
}
}

View File

@@ -0,0 +1,163 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Models;
namespace StellaOps.PacksRegistry.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="IAttestationRepository"/>.
/// </summary>
public sealed class PostgresAttestationRepository : RepositoryBase<PacksRegistryDataSource>, IAttestationRepository
{
private bool _tableInitialized;
public PostgresAttestationRepository(PacksRegistryDataSource dataSource, ILogger<PostgresAttestationRepository> logger)
: base(dataSource, logger)
{
}
public async Task UpsertAsync(AttestationRecord record, byte[] content, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(content);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
INSERT INTO packs.attestations (pack_id, tenant_id, type, digest, content, notes, created_at)
VALUES (@pack_id, @tenant_id, @type, @digest, @content, @notes, @created_at)
ON CONFLICT (pack_id, type) DO UPDATE SET
tenant_id = EXCLUDED.tenant_id,
digest = EXCLUDED.digest,
content = EXCLUDED.content,
notes = EXCLUDED.notes,
created_at = EXCLUDED.created_at";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", record.PackId);
AddParameter(command, "@tenant_id", record.TenantId);
AddParameter(command, "@type", record.Type);
AddParameter(command, "@digest", record.Digest);
AddParameter(command, "@content", content);
AddParameter(command, "@notes", (object?)record.Notes ?? DBNull.Value);
AddParameter(command, "@created_at", record.CreatedAtUtc);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<AttestationRecord?> GetAsync(string packId, string type, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
ArgumentException.ThrowIfNullOrWhiteSpace(type);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT pack_id, tenant_id, type, digest, notes, created_at
FROM packs.attestations
WHERE pack_id = @pack_id AND type = @type";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
AddParameter(command, "@type", type.Trim());
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return null;
}
return MapAttestationRecord(reader);
}
public async Task<IReadOnlyList<AttestationRecord>> ListAsync(string packId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT pack_id, tenant_id, type, digest, notes, created_at
FROM packs.attestations
WHERE pack_id = @pack_id
ORDER BY created_at DESC";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var results = new List<AttestationRecord>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(MapAttestationRecord(reader));
}
return results;
}
public async Task<byte[]?> GetContentAsync(string packId, string type, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
ArgumentException.ThrowIfNullOrWhiteSpace(type);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = "SELECT content FROM packs.attestations WHERE pack_id = @pack_id AND type = @type";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
AddParameter(command, "@type", type.Trim());
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return result is byte[] bytes ? bytes : null;
}
private static AttestationRecord MapAttestationRecord(NpgsqlDataReader reader)
{
return new AttestationRecord(
PackId: reader.GetString(0),
TenantId: reader.GetString(1),
Type: reader.GetString(2),
Digest: reader.GetString(3),
CreatedAtUtc: reader.GetFieldValue<DateTimeOffset>(5),
Notes: reader.IsDBNull(4) ? null : reader.GetString(4));
}
private async Task EnsureTableAsync(CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = @"
CREATE SCHEMA IF NOT EXISTS packs;
CREATE TABLE IF NOT EXISTS packs.attestations (
pack_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
type TEXT NOT NULL,
digest TEXT NOT NULL,
content BYTEA NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (pack_id, type)
);
CREATE INDEX IF NOT EXISTS idx_attestations_tenant_id ON packs.attestations (tenant_id);
CREATE INDEX IF NOT EXISTS idx_attestations_created_at ON packs.attestations (created_at DESC);";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(ddl, connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,120 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Models;
namespace StellaOps.PacksRegistry.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="IAuditRepository"/>.
/// Append-only audit log for registry actions.
/// </summary>
public sealed class PostgresAuditRepository : RepositoryBase<PacksRegistryDataSource>, IAuditRepository
{
private bool _tableInitialized;
public PostgresAuditRepository(PacksRegistryDataSource dataSource, ILogger<PostgresAuditRepository> logger)
: base(dataSource, logger)
{
}
public async Task AppendAsync(AuditRecord record, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(record);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
INSERT INTO packs.audit_log (id, pack_id, tenant_id, event, actor, notes, occurred_at)
VALUES (@id, @pack_id, @tenant_id, @event, @actor, @notes, @occurred_at)";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@id", Guid.NewGuid().ToString("N"));
AddParameter(command, "@pack_id", (object?)record.PackId ?? DBNull.Value);
AddParameter(command, "@tenant_id", record.TenantId);
AddParameter(command, "@event", record.Event);
AddParameter(command, "@actor", (object?)record.Actor ?? DBNull.Value);
AddParameter(command, "@notes", (object?)record.Notes ?? DBNull.Value);
AddParameter(command, "@occurred_at", record.OccurredAtUtc);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<AuditRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
var sql = @"
SELECT pack_id, tenant_id, event, occurred_at, actor, notes
FROM packs.audit_log";
if (!string.IsNullOrWhiteSpace(tenantId))
{
sql += " WHERE tenant_id = @tenant_id";
}
sql += " ORDER BY occurred_at DESC";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
if (!string.IsNullOrWhiteSpace(tenantId))
{
AddParameter(command, "@tenant_id", tenantId.Trim());
}
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var results = new List<AuditRecord>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(MapAuditRecord(reader));
}
return results;
}
private static AuditRecord MapAuditRecord(NpgsqlDataReader reader)
{
return new AuditRecord(
PackId: reader.IsDBNull(0) ? null : reader.GetString(0),
TenantId: reader.GetString(1),
Event: reader.GetString(2),
OccurredAtUtc: reader.GetFieldValue<DateTimeOffset>(3),
Actor: reader.IsDBNull(4) ? null : reader.GetString(4),
Notes: reader.IsDBNull(5) ? null : reader.GetString(5));
}
private async Task EnsureTableAsync(CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = @"
CREATE SCHEMA IF NOT EXISTS packs;
CREATE TABLE IF NOT EXISTS packs.audit_log (
id TEXT PRIMARY KEY,
pack_id TEXT,
tenant_id TEXT NOT NULL,
event TEXT NOT NULL,
actor TEXT,
notes TEXT,
occurred_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_id ON packs.audit_log (tenant_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_pack_id ON packs.audit_log (pack_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_occurred_at ON packs.audit_log (occurred_at DESC);";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(ddl, connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,143 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Models;
namespace StellaOps.PacksRegistry.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="ILifecycleRepository"/>.
/// </summary>
public sealed class PostgresLifecycleRepository : RepositoryBase<PacksRegistryDataSource>, ILifecycleRepository
{
private bool _tableInitialized;
public PostgresLifecycleRepository(PacksRegistryDataSource dataSource, ILogger<PostgresLifecycleRepository> logger)
: base(dataSource, logger)
{
}
public async Task UpsertAsync(LifecycleRecord record, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(record);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
INSERT INTO packs.lifecycles (pack_id, tenant_id, state, notes, updated_at)
VALUES (@pack_id, @tenant_id, @state, @notes, @updated_at)
ON CONFLICT (pack_id) DO UPDATE SET
tenant_id = EXCLUDED.tenant_id,
state = EXCLUDED.state,
notes = EXCLUDED.notes,
updated_at = EXCLUDED.updated_at";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", record.PackId);
AddParameter(command, "@tenant_id", record.TenantId);
AddParameter(command, "@state", record.State);
AddParameter(command, "@notes", (object?)record.Notes ?? DBNull.Value);
AddParameter(command, "@updated_at", record.UpdatedAtUtc);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<LifecycleRecord?> GetAsync(string packId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT pack_id, tenant_id, state, notes, updated_at
FROM packs.lifecycles
WHERE pack_id = @pack_id";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return null;
}
return MapLifecycleRecord(reader);
}
public async Task<IReadOnlyList<LifecycleRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
var sql = @"
SELECT pack_id, tenant_id, state, notes, updated_at
FROM packs.lifecycles";
if (!string.IsNullOrWhiteSpace(tenantId))
{
sql += " WHERE tenant_id = @tenant_id";
}
sql += " ORDER BY updated_at DESC";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
if (!string.IsNullOrWhiteSpace(tenantId))
{
AddParameter(command, "@tenant_id", tenantId.Trim());
}
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var results = new List<LifecycleRecord>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(MapLifecycleRecord(reader));
}
return results;
}
private static LifecycleRecord MapLifecycleRecord(NpgsqlDataReader reader)
{
return new LifecycleRecord(
PackId: reader.GetString(0),
TenantId: reader.GetString(1),
State: reader.GetString(2),
Notes: reader.IsDBNull(3) ? null : reader.GetString(3),
UpdatedAtUtc: reader.GetFieldValue<DateTimeOffset>(4));
}
private async Task EnsureTableAsync(CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = @"
CREATE SCHEMA IF NOT EXISTS packs;
CREATE TABLE IF NOT EXISTS packs.lifecycles (
pack_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
state TEXT NOT NULL,
notes TEXT,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_lifecycles_tenant_id ON packs.lifecycles (tenant_id);
CREATE INDEX IF NOT EXISTS idx_lifecycles_state ON packs.lifecycles (state);
CREATE INDEX IF NOT EXISTS idx_lifecycles_updated_at ON packs.lifecycles (updated_at DESC);";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(ddl, connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,155 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Models;
namespace StellaOps.PacksRegistry.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="IMirrorRepository"/>.
/// </summary>
public sealed class PostgresMirrorRepository : RepositoryBase<PacksRegistryDataSource>, IMirrorRepository
{
private bool _tableInitialized;
public PostgresMirrorRepository(PacksRegistryDataSource dataSource, ILogger<PostgresMirrorRepository> logger)
: base(dataSource, logger)
{
}
public async Task UpsertAsync(MirrorSourceRecord record, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(record);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
INSERT INTO packs.mirror_sources (id, tenant_id, upstream_uri, enabled, status, notes, updated_at, last_successful_sync_at)
VALUES (@id, @tenant_id, @upstream_uri, @enabled, @status, @notes, @updated_at, @last_successful_sync_at)
ON CONFLICT (id) DO UPDATE SET
tenant_id = EXCLUDED.tenant_id,
upstream_uri = EXCLUDED.upstream_uri,
enabled = EXCLUDED.enabled,
status = EXCLUDED.status,
notes = EXCLUDED.notes,
updated_at = EXCLUDED.updated_at,
last_successful_sync_at = EXCLUDED.last_successful_sync_at";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@id", record.Id);
AddParameter(command, "@tenant_id", record.TenantId);
AddParameter(command, "@upstream_uri", record.UpstreamUri.ToString());
AddParameter(command, "@enabled", record.Enabled);
AddParameter(command, "@status", record.Status);
AddParameter(command, "@notes", (object?)record.Notes ?? DBNull.Value);
AddParameter(command, "@updated_at", record.UpdatedAtUtc);
AddParameter(command, "@last_successful_sync_at", record.LastSuccessfulSyncUtc.HasValue ? record.LastSuccessfulSyncUtc.Value : DBNull.Value);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<MirrorSourceRecord?> GetAsync(string id, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT id, tenant_id, upstream_uri, enabled, status, updated_at, notes, last_successful_sync_at
FROM packs.mirror_sources
WHERE id = @id";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@id", id.Trim());
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return null;
}
return MapMirrorSourceRecord(reader);
}
public async Task<IReadOnlyList<MirrorSourceRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
var sql = @"
SELECT id, tenant_id, upstream_uri, enabled, status, updated_at, notes, last_successful_sync_at
FROM packs.mirror_sources";
if (!string.IsNullOrWhiteSpace(tenantId))
{
sql += " WHERE tenant_id = @tenant_id";
}
sql += " ORDER BY updated_at DESC";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
if (!string.IsNullOrWhiteSpace(tenantId))
{
AddParameter(command, "@tenant_id", tenantId.Trim());
}
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var results = new List<MirrorSourceRecord>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(MapMirrorSourceRecord(reader));
}
return results;
}
private static MirrorSourceRecord MapMirrorSourceRecord(NpgsqlDataReader reader)
{
return new MirrorSourceRecord(
Id: reader.GetString(0),
TenantId: reader.GetString(1),
UpstreamUri: new Uri(reader.GetString(2)),
Enabled: reader.GetBoolean(3),
Status: reader.GetString(4),
UpdatedAtUtc: reader.GetFieldValue<DateTimeOffset>(5),
Notes: reader.IsDBNull(6) ? null : reader.GetString(6),
LastSuccessfulSyncUtc: reader.IsDBNull(7) ? null : reader.GetFieldValue<DateTimeOffset>(7));
}
private async Task EnsureTableAsync(CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = @"
CREATE SCHEMA IF NOT EXISTS packs;
CREATE TABLE IF NOT EXISTS packs.mirror_sources (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
upstream_uri TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
status TEXT NOT NULL,
notes TEXT,
updated_at TIMESTAMPTZ NOT NULL,
last_successful_sync_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_mirror_sources_tenant_id ON packs.mirror_sources (tenant_id);
CREATE INDEX IF NOT EXISTS idx_mirror_sources_enabled ON packs.mirror_sources (enabled);
CREATE INDEX IF NOT EXISTS idx_mirror_sources_updated_at ON packs.mirror_sources (updated_at DESC);";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(ddl, connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,215 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Models;
namespace StellaOps.PacksRegistry.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="IPackRepository"/>.
/// </summary>
public sealed class PostgresPackRepository : RepositoryBase<PacksRegistryDataSource>, IPackRepository
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
private bool _tableInitialized;
public PostgresPackRepository(PacksRegistryDataSource dataSource, ILogger<PostgresPackRepository> logger)
: base(dataSource, logger)
{
}
public async Task UpsertAsync(PackRecord record, byte[] content, byte[]? provenance, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(content);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
INSERT INTO packs.packs (pack_id, name, version, tenant_id, digest, signature, provenance_uri, provenance_digest, metadata, content, provenance, created_at)
VALUES (@pack_id, @name, @version, @tenant_id, @digest, @signature, @provenance_uri, @provenance_digest, @metadata, @content, @provenance, @created_at)
ON CONFLICT (pack_id) DO UPDATE SET
name = EXCLUDED.name,
version = EXCLUDED.version,
tenant_id = EXCLUDED.tenant_id,
digest = EXCLUDED.digest,
signature = EXCLUDED.signature,
provenance_uri = EXCLUDED.provenance_uri,
provenance_digest = EXCLUDED.provenance_digest,
metadata = EXCLUDED.metadata,
content = EXCLUDED.content,
provenance = EXCLUDED.provenance";
var metadataJson = record.Metadata is null ? null : JsonSerializer.Serialize(record.Metadata, JsonOptions);
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", record.PackId);
AddParameter(command, "@name", record.Name);
AddParameter(command, "@version", record.Version);
AddParameter(command, "@tenant_id", record.TenantId);
AddParameter(command, "@digest", record.Digest);
AddParameter(command, "@signature", (object?)record.Signature ?? DBNull.Value);
AddParameter(command, "@provenance_uri", (object?)record.ProvenanceUri ?? DBNull.Value);
AddParameter(command, "@provenance_digest", (object?)record.ProvenanceDigest ?? DBNull.Value);
AddJsonbParameter(command, "@metadata", metadataJson);
AddParameter(command, "@content", content);
AddParameter(command, "@provenance", (object?)provenance ?? DBNull.Value);
AddParameter(command, "@created_at", record.CreatedAtUtc);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<PackRecord?> GetAsync(string packId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT pack_id, name, version, tenant_id, digest, signature, provenance_uri, provenance_digest, metadata, created_at
FROM packs.packs
WHERE pack_id = @pack_id";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return null;
}
return MapPackRecord(reader);
}
public async Task<IReadOnlyList<PackRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
var sql = @"
SELECT pack_id, name, version, tenant_id, digest, signature, provenance_uri, provenance_digest, metadata, created_at
FROM packs.packs";
if (!string.IsNullOrWhiteSpace(tenantId))
{
sql += " WHERE tenant_id = @tenant_id";
}
sql += " ORDER BY created_at DESC";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
if (!string.IsNullOrWhiteSpace(tenantId))
{
AddParameter(command, "@tenant_id", tenantId.Trim());
}
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var results = new List<PackRecord>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(MapPackRecord(reader));
}
return results;
}
public async Task<byte[]?> GetContentAsync(string packId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = "SELECT content FROM packs.packs WHERE pack_id = @pack_id";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return result is byte[] bytes ? bytes : null;
}
public async Task<byte[]?> GetProvenanceAsync(string packId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = "SELECT provenance FROM packs.packs WHERE pack_id = @pack_id";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return result is byte[] bytes ? bytes : null;
}
private static PackRecord MapPackRecord(NpgsqlDataReader reader)
{
var metadataJson = reader.IsDBNull(8) ? null : reader.GetString(8);
var metadata = string.IsNullOrWhiteSpace(metadataJson)
? null
: JsonSerializer.Deserialize<Dictionary<string, string>>(metadataJson, JsonOptions);
return new PackRecord(
PackId: reader.GetString(0),
Name: reader.GetString(1),
Version: reader.GetString(2),
TenantId: reader.GetString(3),
Digest: reader.GetString(4),
Signature: reader.IsDBNull(5) ? null : reader.GetString(5),
ProvenanceUri: reader.IsDBNull(6) ? null : reader.GetString(6),
ProvenanceDigest: reader.IsDBNull(7) ? null : reader.GetString(7),
CreatedAtUtc: reader.GetFieldValue<DateTimeOffset>(9),
Metadata: metadata);
}
private async Task EnsureTableAsync(CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = @"
CREATE SCHEMA IF NOT EXISTS packs;
CREATE TABLE IF NOT EXISTS packs.packs (
pack_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
version TEXT NOT NULL,
tenant_id TEXT NOT NULL,
digest TEXT NOT NULL,
signature TEXT,
provenance_uri TEXT,
provenance_digest TEXT,
metadata JSONB,
content BYTEA NOT NULL,
provenance BYTEA,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_packs_tenant_id ON packs.packs (tenant_id);
CREATE INDEX IF NOT EXISTS idx_packs_name_version ON packs.packs (name, version);
CREATE INDEX IF NOT EXISTS idx_packs_created_at ON packs.packs (created_at DESC);";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(ddl, connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,143 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Repositories;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Core.Models;
namespace StellaOps.PacksRegistry.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL implementation of <see cref="IParityRepository"/>.
/// </summary>
public sealed class PostgresParityRepository : RepositoryBase<PacksRegistryDataSource>, IParityRepository
{
private bool _tableInitialized;
public PostgresParityRepository(PacksRegistryDataSource dataSource, ILogger<PostgresParityRepository> logger)
: base(dataSource, logger)
{
}
public async Task UpsertAsync(ParityRecord record, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(record);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
INSERT INTO packs.parities (pack_id, tenant_id, status, notes, updated_at)
VALUES (@pack_id, @tenant_id, @status, @notes, @updated_at)
ON CONFLICT (pack_id) DO UPDATE SET
tenant_id = EXCLUDED.tenant_id,
status = EXCLUDED.status,
notes = EXCLUDED.notes,
updated_at = EXCLUDED.updated_at";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", record.PackId);
AddParameter(command, "@tenant_id", record.TenantId);
AddParameter(command, "@status", record.Status);
AddParameter(command, "@notes", (object?)record.Notes ?? DBNull.Value);
AddParameter(command, "@updated_at", record.UpdatedAtUtc);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
public async Task<ParityRecord?> GetAsync(string packId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packId);
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
const string sql = @"
SELECT pack_id, tenant_id, status, notes, updated_at
FROM packs.parities
WHERE pack_id = @pack_id";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
AddParameter(command, "@pack_id", packId.Trim());
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
return null;
}
return MapParityRecord(reader);
}
public async Task<IReadOnlyList<ParityRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
await EnsureTableAsync(cancellationToken).ConfigureAwait(false);
var sql = @"
SELECT pack_id, tenant_id, status, notes, updated_at
FROM packs.parities";
if (!string.IsNullOrWhiteSpace(tenantId))
{
sql += " WHERE tenant_id = @tenant_id";
}
sql += " ORDER BY updated_at DESC";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(sql, connection);
if (!string.IsNullOrWhiteSpace(tenantId))
{
AddParameter(command, "@tenant_id", tenantId.Trim());
}
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var results = new List<ParityRecord>();
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(MapParityRecord(reader));
}
return results;
}
private static ParityRecord MapParityRecord(NpgsqlDataReader reader)
{
return new ParityRecord(
PackId: reader.GetString(0),
TenantId: reader.GetString(1),
Status: reader.GetString(2),
Notes: reader.IsDBNull(3) ? null : reader.GetString(3),
UpdatedAtUtc: reader.GetFieldValue<DateTimeOffset>(4));
}
private async Task EnsureTableAsync(CancellationToken cancellationToken)
{
if (_tableInitialized)
{
return;
}
const string ddl = @"
CREATE SCHEMA IF NOT EXISTS packs;
CREATE TABLE IF NOT EXISTS packs.parities (
pack_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
status TEXT NOT NULL,
notes TEXT,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_parities_tenant_id ON packs.parities (tenant_id);
CREATE INDEX IF NOT EXISTS idx_parities_status ON packs.parities (status);
CREATE INDEX IF NOT EXISTS idx_parities_updated_at ON packs.parities (updated_at DESC);";
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
await using var command = CreateCommand(ddl, connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_tableInitialized = true;
}
}

View File

@@ -0,0 +1,63 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.PacksRegistry.Core.Contracts;
using StellaOps.PacksRegistry.Storage.Postgres.Repositories;
namespace StellaOps.PacksRegistry.Storage.Postgres;
/// <summary>
/// Extension methods for configuring PacksRegistry PostgreSQL storage services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds PacksRegistry PostgreSQL storage services.
/// </summary>
/// <param name="services">Service collection.</param>
/// <param name="configuration">Configuration root.</param>
/// <param name="sectionName">Configuration section name for PostgreSQL options.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddPacksRegistryPostgresStorage(
this IServiceCollection services,
IConfiguration configuration,
string sectionName = "Postgres:PacksRegistry")
{
services.Configure<PostgresOptions>(configuration.GetSection(sectionName));
services.AddSingleton<PacksRegistryDataSource>();
// Register repositories
services.AddSingleton<IPackRepository, PostgresPackRepository>();
services.AddSingleton<IAttestationRepository, PostgresAttestationRepository>();
services.AddSingleton<IAuditRepository, PostgresAuditRepository>();
services.AddSingleton<ILifecycleRepository, PostgresLifecycleRepository>();
services.AddSingleton<IMirrorRepository, PostgresMirrorRepository>();
services.AddSingleton<IParityRepository, PostgresParityRepository>();
return services;
}
/// <summary>
/// Adds PacksRegistry PostgreSQL storage services with explicit options.
/// </summary>
/// <param name="services">Service collection.</param>
/// <param name="configureOptions">Options configuration action.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddPacksRegistryPostgresStorage(
this IServiceCollection services,
Action<PostgresOptions> configureOptions)
{
services.Configure(configureOptions);
services.AddSingleton<PacksRegistryDataSource>();
// Register repositories
services.AddSingleton<IPackRepository, PostgresPackRepository>();
services.AddSingleton<IAttestationRepository, PostgresAttestationRepository>();
services.AddSingleton<IAuditRepository, PostgresAuditRepository>();
services.AddSingleton<ILifecycleRepository, PostgresLifecycleRepository>();
services.AddSingleton<IMirrorRepository, PostgresMirrorRepository>();
services.AddSingleton<IParityRepository, PostgresParityRepository>();
return services;
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<RootNamespace>StellaOps.PacksRegistry.Storage.Postgres</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.PacksRegistry.Core/StellaOps.PacksRegistry.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Infrastructure.Postgres/StellaOps.Infrastructure.Postgres.csproj" />
</ItemGroup>
</Project>