Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -0,0 +1,35 @@
using StellaOps.Integrations.Core;
namespace StellaOps.Integrations.Persistence;
/// <summary>
/// Repository contract for integration persistence.
/// </summary>
public interface IIntegrationRepository
{
Task<Integration?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Integration>> GetAllAsync(IntegrationQuery query, CancellationToken cancellationToken = default);
Task<int> CountAsync(IntegrationQuery query, CancellationToken cancellationToken = default);
Task<Integration> CreateAsync(Integration integration, CancellationToken cancellationToken = default);
Task<Integration> UpdateAsync(Integration integration, CancellationToken cancellationToken = default);
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Integration>> GetByProviderAsync(IntegrationProvider provider, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Integration>> GetActiveByTypeAsync(IntegrationType type, CancellationToken cancellationToken = default);
Task UpdateHealthStatusAsync(Guid id, HealthStatus status, DateTimeOffset checkedAt, CancellationToken cancellationToken = default);
}
/// <summary>
/// Query parameters for repository operations.
/// </summary>
public sealed record IntegrationQuery(
IntegrationType? Type = null,
IntegrationProvider? Provider = null,
IntegrationStatus? Status = null,
string? Search = null,
IReadOnlyList<string>? Tags = null,
string? TenantId = null,
bool IncludeDeleted = false,
int Skip = 0,
int Take = 20,
string SortBy = "name",
bool SortDescending = false);

View File

@@ -0,0 +1,83 @@
using Microsoft.EntityFrameworkCore;
using StellaOps.Integrations.Core;
namespace StellaOps.Integrations.Persistence;
/// <summary>
/// EF Core DbContext for Integration persistence.
/// </summary>
public sealed class IntegrationDbContext : DbContext
{
public IntegrationDbContext(DbContextOptions<IntegrationDbContext> options)
: base(options)
{
}
public DbSet<IntegrationEntity> Integrations => Set<IntegrationEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IntegrationEntity>(entity =>
{
entity.ToTable("integrations");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(256).IsRequired();
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(1024);
entity.Property(e => e.Type).HasColumnName("type").IsRequired();
entity.Property(e => e.Provider).HasColumnName("provider").IsRequired();
entity.Property(e => e.Status).HasColumnName("status").IsRequired();
entity.Property(e => e.Endpoint).HasColumnName("endpoint").HasMaxLength(2048).IsRequired();
entity.Property(e => e.AuthRefUri).HasColumnName("auth_ref_uri").HasMaxLength(1024);
entity.Property(e => e.OrganizationId).HasColumnName("organization_id").HasMaxLength(256);
entity.Property(e => e.ConfigJson).HasColumnName("config_json").HasColumnType("jsonb");
entity.Property(e => e.LastHealthStatus).HasColumnName("last_health_status");
entity.Property(e => e.LastHealthCheckAt).HasColumnName("last_health_check_at");
entity.Property(e => e.CreatedAt).HasColumnName("created_at").IsRequired();
entity.Property(e => e.UpdatedAt).HasColumnName("updated_at").IsRequired();
entity.Property(e => e.CreatedBy).HasColumnName("created_by").HasMaxLength(256);
entity.Property(e => e.UpdatedBy).HasColumnName("updated_by").HasMaxLength(256);
entity.Property(e => e.TenantId).HasColumnName("tenant_id").HasMaxLength(128);
entity.Property(e => e.TagsJson).HasColumnName("tags").HasColumnType("jsonb");
entity.Property(e => e.IsDeleted).HasColumnName("is_deleted").IsRequired();
entity.HasIndex(e => e.Type);
entity.HasIndex(e => e.Provider);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.TenantId);
entity.HasIndex(e => new { e.TenantId, e.Name }).IsUnique().HasFilter("is_deleted = false");
});
}
}
/// <summary>
/// EF Core entity for Integration.
/// </summary>
public sealed class IntegrationEntity
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public IntegrationType Type { get; set; }
public IntegrationProvider Provider { get; set; }
public IntegrationStatus Status { get; set; }
public string Endpoint { get; set; } = string.Empty;
public string? AuthRefUri { get; set; }
public string? OrganizationId { get; set; }
public string? ConfigJson { get; set; }
public HealthStatus LastHealthStatus { get; set; }
public DateTimeOffset? LastHealthCheckAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public string? CreatedBy { get; set; }
public string? UpdatedBy { get; set; }
public string? TenantId { get; set; }
public string? TagsJson { get; set; }
public bool IsDeleted { get; set; }
}

View File

@@ -0,0 +1,229 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using StellaOps.Integrations.Core;
namespace StellaOps.Integrations.Persistence;
/// <summary>
/// PostgreSQL implementation of integration repository.
/// </summary>
public sealed class PostgresIntegrationRepository : IIntegrationRepository
{
private readonly IntegrationDbContext _context;
public PostgresIntegrationRepository(IntegrationDbContext context)
{
_context = context;
}
public async Task<Integration?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
var entity = await _context.Integrations
.AsNoTracking()
.FirstOrDefaultAsync(e => e.Id == id && !e.IsDeleted, cancellationToken);
return entity is null ? null : MapToDomain(entity);
}
public async Task<IReadOnlyList<Integration>> GetAllAsync(IntegrationQuery query, CancellationToken cancellationToken = default)
{
var dbQuery = BuildQuery(query);
dbQuery = query.SortBy.ToLowerInvariant() switch
{
"name" => query.SortDescending ? dbQuery.OrderByDescending(e => e.Name) : dbQuery.OrderBy(e => e.Name),
"createdat" => query.SortDescending ? dbQuery.OrderByDescending(e => e.CreatedAt) : dbQuery.OrderBy(e => e.CreatedAt),
"updatedat" => query.SortDescending ? dbQuery.OrderByDescending(e => e.UpdatedAt) : dbQuery.OrderBy(e => e.UpdatedAt),
"status" => query.SortDescending ? dbQuery.OrderByDescending(e => e.Status) : dbQuery.OrderBy(e => e.Status),
_ => dbQuery.OrderBy(e => e.Name)
};
var entities = await dbQuery
.Skip(query.Skip)
.Take(query.Take)
.AsNoTracking()
.ToListAsync(cancellationToken);
return entities.Select(MapToDomain).ToList();
}
public async Task<int> CountAsync(IntegrationQuery query, CancellationToken cancellationToken = default)
{
return await BuildQuery(query).CountAsync(cancellationToken);
}
public async Task<Integration> CreateAsync(Integration integration, CancellationToken cancellationToken = default)
{
var entity = MapToEntity(integration);
_context.Integrations.Add(entity);
await _context.SaveChangesAsync(cancellationToken);
return MapToDomain(entity);
}
public async Task<Integration> UpdateAsync(Integration integration, CancellationToken cancellationToken = default)
{
var entity = await _context.Integrations
.FirstOrDefaultAsync(e => e.Id == integration.Id, cancellationToken)
?? throw new InvalidOperationException($"Integration {integration.Id} not found");
entity.Name = integration.Name;
entity.Description = integration.Description;
entity.Status = integration.Status;
entity.Endpoint = integration.Endpoint;
entity.AuthRefUri = integration.AuthRefUri;
entity.OrganizationId = integration.OrganizationId;
entity.ConfigJson = integration.ConfigJson;
entity.LastHealthStatus = integration.LastHealthStatus;
entity.LastHealthCheckAt = integration.LastHealthCheckAt;
entity.UpdatedAt = integration.UpdatedAt;
entity.UpdatedBy = integration.UpdatedBy;
entity.TagsJson = integration.Tags.Count > 0 ? JsonSerializer.Serialize(integration.Tags) : null;
entity.IsDeleted = integration.IsDeleted;
await _context.SaveChangesAsync(cancellationToken);
return MapToDomain(entity);
}
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
{
var entity = await _context.Integrations
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
if (entity is not null)
{
entity.IsDeleted = true;
entity.Status = IntegrationStatus.Archived;
entity.UpdatedAt = DateTimeOffset.UtcNow;
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task<IReadOnlyList<Integration>> GetByProviderAsync(IntegrationProvider provider, CancellationToken cancellationToken = default)
{
var entities = await _context.Integrations
.Where(e => e.Provider == provider && !e.IsDeleted)
.AsNoTracking()
.ToListAsync(cancellationToken);
return entities.Select(MapToDomain).ToList();
}
public async Task<IReadOnlyList<Integration>> GetActiveByTypeAsync(IntegrationType type, CancellationToken cancellationToken = default)
{
var entities = await _context.Integrations
.Where(e => e.Type == type && e.Status == IntegrationStatus.Active && !e.IsDeleted)
.AsNoTracking()
.ToListAsync(cancellationToken);
return entities.Select(MapToDomain).ToList();
}
public async Task UpdateHealthStatusAsync(Guid id, HealthStatus status, DateTimeOffset checkedAt, CancellationToken cancellationToken = default)
{
var entity = await _context.Integrations
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
if (entity is not null)
{
entity.LastHealthStatus = status;
entity.LastHealthCheckAt = checkedAt;
await _context.SaveChangesAsync(cancellationToken);
}
}
private IQueryable<IntegrationEntity> BuildQuery(IntegrationQuery query)
{
var dbQuery = _context.Integrations.AsQueryable();
if (!query.IncludeDeleted)
{
dbQuery = dbQuery.Where(e => !e.IsDeleted);
}
if (query.TenantId is not null)
{
dbQuery = dbQuery.Where(e => e.TenantId == query.TenantId);
}
if (query.Type.HasValue)
{
dbQuery = dbQuery.Where(e => e.Type == query.Type.Value);
}
if (query.Provider.HasValue)
{
dbQuery = dbQuery.Where(e => e.Provider == query.Provider.Value);
}
if (query.Status.HasValue)
{
dbQuery = dbQuery.Where(e => e.Status == query.Status.Value);
}
if (!string.IsNullOrWhiteSpace(query.Search))
{
var searchLower = query.Search.ToLowerInvariant();
dbQuery = dbQuery.Where(e =>
e.Name.ToLower().Contains(searchLower) ||
(e.Description != null && e.Description.ToLower().Contains(searchLower)));
}
return dbQuery;
}
private static Integration MapToDomain(IntegrationEntity entity)
{
var tags = string.IsNullOrEmpty(entity.TagsJson)
? new List<string>()
: JsonSerializer.Deserialize<List<string>>(entity.TagsJson) ?? new List<string>();
return new Integration
{
Id = entity.Id,
Name = entity.Name,
Description = entity.Description,
Type = entity.Type,
Provider = entity.Provider,
Status = entity.Status,
Endpoint = entity.Endpoint,
AuthRefUri = entity.AuthRefUri,
OrganizationId = entity.OrganizationId,
ConfigJson = entity.ConfigJson,
LastHealthStatus = entity.LastHealthStatus,
LastHealthCheckAt = entity.LastHealthCheckAt,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
CreatedBy = entity.CreatedBy,
UpdatedBy = entity.UpdatedBy,
TenantId = entity.TenantId,
Tags = tags,
IsDeleted = entity.IsDeleted
};
}
private static IntegrationEntity MapToEntity(Integration integration)
{
return new IntegrationEntity
{
Id = integration.Id,
Name = integration.Name,
Description = integration.Description,
Type = integration.Type,
Provider = integration.Provider,
Status = integration.Status,
Endpoint = integration.Endpoint,
AuthRefUri = integration.AuthRefUri,
OrganizationId = integration.OrganizationId,
ConfigJson = integration.ConfigJson,
LastHealthStatus = integration.LastHealthStatus,
LastHealthCheckAt = integration.LastHealthCheckAt,
CreatedAt = integration.CreatedAt,
UpdatedAt = integration.UpdatedAt,
CreatedBy = integration.CreatedBy,
UpdatedBy = integration.UpdatedBy,
TenantId = integration.TenantId,
TagsJson = integration.Tags.Count > 0 ? JsonSerializer.Serialize(integration.Tags) : null,
IsDeleted = integration.IsDeleted
};
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<RootNamespace>StellaOps.Integrations.Persistence</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Integrations.Core\StellaOps.Integrations.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
</Project>