Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
using StellaOps.Integrations.Core;
|
||||
using StellaOps.Plugin;
|
||||
|
||||
namespace StellaOps.Integrations.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin contract for integration connectors.
|
||||
/// Each provider (GitHub, Harbor, ECR, etc.) implements this interface.
|
||||
/// </summary>
|
||||
public interface IIntegrationConnectorPlugin : IAvailabilityPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration type this plugin handles.
|
||||
/// </summary>
|
||||
IntegrationType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific provider implementation.
|
||||
/// </summary>
|
||||
IntegrationProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tests connectivity and authentication to the integration endpoint.
|
||||
/// </summary>
|
||||
/// <param name="config">Configuration including resolved secrets.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure with details.</returns>
|
||||
Task<TestConnectionResult> TestConnectionAsync(IntegrationConfig config, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a health check on the integration endpoint.
|
||||
/// </summary>
|
||||
/// <param name="config">Configuration including resolved secrets.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Health check result with status and details.</returns>
|
||||
Task<HealthCheckResult> CheckHealthAsync(IntegrationConfig config, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using StellaOps.Integrations.Core;
|
||||
|
||||
namespace StellaOps.Integrations.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for creating an integration.
|
||||
/// </summary>
|
||||
public sealed record CreateIntegrationRequest(
|
||||
string Name,
|
||||
string? Description,
|
||||
IntegrationType Type,
|
||||
IntegrationProvider Provider,
|
||||
string Endpoint,
|
||||
string? AuthRefUri,
|
||||
string? OrganizationId,
|
||||
IReadOnlyDictionary<string, object>? ExtendedConfig,
|
||||
IReadOnlyList<string>? Tags);
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for updating an integration.
|
||||
/// </summary>
|
||||
public sealed record UpdateIntegrationRequest(
|
||||
string? Name,
|
||||
string? Description,
|
||||
string? Endpoint,
|
||||
string? AuthRefUri,
|
||||
string? OrganizationId,
|
||||
IReadOnlyDictionary<string, object>? ExtendedConfig,
|
||||
IReadOnlyList<string>? Tags,
|
||||
IntegrationStatus? Status);
|
||||
|
||||
/// <summary>
|
||||
/// Response DTO for integration details.
|
||||
/// </summary>
|
||||
public sealed record IntegrationResponse(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? Description,
|
||||
IntegrationType Type,
|
||||
IntegrationProvider Provider,
|
||||
IntegrationStatus Status,
|
||||
string Endpoint,
|
||||
bool HasAuth,
|
||||
string? OrganizationId,
|
||||
HealthStatus LastHealthStatus,
|
||||
DateTimeOffset? LastHealthCheckAt,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt,
|
||||
string? CreatedBy,
|
||||
string? UpdatedBy,
|
||||
IReadOnlyList<string> Tags);
|
||||
|
||||
/// <summary>
|
||||
/// Response DTO for test-connection operation.
|
||||
/// </summary>
|
||||
public sealed record TestConnectionResponse(
|
||||
Guid IntegrationId,
|
||||
bool Success,
|
||||
string? Message,
|
||||
IReadOnlyDictionary<string, string>? Details,
|
||||
TimeSpan Duration,
|
||||
DateTimeOffset TestedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Response DTO for health check operation.
|
||||
/// </summary>
|
||||
public sealed record HealthCheckResponse(
|
||||
Guid IntegrationId,
|
||||
HealthStatus Status,
|
||||
string? Message,
|
||||
IReadOnlyDictionary<string, string>? Details,
|
||||
DateTimeOffset CheckedAt,
|
||||
TimeSpan Duration);
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for listing integrations.
|
||||
/// </summary>
|
||||
public sealed record ListIntegrationsQuery(
|
||||
IntegrationType? Type = null,
|
||||
IntegrationProvider? Provider = null,
|
||||
IntegrationStatus? Status = null,
|
||||
string? Search = null,
|
||||
IReadOnlyList<string>? Tags = null,
|
||||
int Page = 1,
|
||||
int PageSize = 20,
|
||||
string SortBy = "name",
|
||||
bool SortDescending = false);
|
||||
|
||||
/// <summary>
|
||||
/// Paginated response for integration listings.
|
||||
/// </summary>
|
||||
public sealed record PagedIntegrationsResponse(
|
||||
IReadOnlyList<IntegrationResponse> Items,
|
||||
int TotalCount,
|
||||
int Page,
|
||||
int PageSize,
|
||||
int TotalPages);
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>StellaOps.Integrations.Contracts</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Integrations.Core\StellaOps.Integrations.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,100 @@
|
||||
namespace StellaOps.Integrations.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Core domain entity representing a configured integration.
|
||||
/// </summary>
|
||||
public sealed class Integration
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable name for the integration.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional description.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Classification of integration by functional purpose.
|
||||
/// </summary>
|
||||
public required IntegrationType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific provider implementation.
|
||||
/// </summary>
|
||||
public required IntegrationProvider Provider { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Lifecycle status.
|
||||
/// </summary>
|
||||
public IntegrationStatus Status { get; set; } = IntegrationStatus.Pending;
|
||||
|
||||
/// <summary>
|
||||
/// Base URL or endpoint for the integration.
|
||||
/// </summary>
|
||||
public required string Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to stored credentials (AuthRef URI, never raw secret).
|
||||
/// Format: authref://{vault}/{path}#{key} or similar.
|
||||
/// </summary>
|
||||
public string? AuthRefUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Organization or tenant identifier within the provider.
|
||||
/// </summary>
|
||||
public string? OrganizationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional provider-specific configuration as JSON.
|
||||
/// </summary>
|
||||
public string? ConfigJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last health check result.
|
||||
/// </summary>
|
||||
public HealthStatus LastHealthStatus { get; set; } = HealthStatus.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp of last health check.
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastHealthCheckAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when the integration was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when the integration was last updated.
|
||||
/// </summary>
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// User or system that created this integration.
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// User or system that last modified this integration.
|
||||
/// </summary>
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant/workspace isolation identifier.
|
||||
/// </summary>
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tags for filtering and grouping.
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Soft-delete marker.
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
namespace StellaOps.Integrations.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Classification of integration by functional purpose.
|
||||
/// </summary>
|
||||
public enum IntegrationType
|
||||
{
|
||||
/// <summary>Container registry (Harbor, ECR, GCR, ACR, Docker Hub, etc.).</summary>
|
||||
Registry = 1,
|
||||
|
||||
/// <summary>Source code management (GitHub, GitLab, Bitbucket, Gitea, etc.).</summary>
|
||||
Scm = 2,
|
||||
|
||||
/// <summary>CI/CD system (GitHub Actions, GitLab CI, Jenkins, etc.).</summary>
|
||||
CiCd = 3,
|
||||
|
||||
/// <summary>Repository source for packages (npm, PyPI, Maven, NuGet, etc.).</summary>
|
||||
RepoSource = 4,
|
||||
|
||||
/// <summary>Runtime host for telemetry (eBPF, ETW, dyld, etc.).</summary>
|
||||
RuntimeHost = 5,
|
||||
|
||||
/// <summary>Advisory/vulnerability feed mirror.</summary>
|
||||
FeedMirror = 6
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specific provider implementation within an integration type.
|
||||
/// </summary>
|
||||
public enum IntegrationProvider
|
||||
{
|
||||
// Registry providers
|
||||
Harbor = 100,
|
||||
Ecr = 101,
|
||||
Gcr = 102,
|
||||
Acr = 103,
|
||||
DockerHub = 104,
|
||||
Quay = 105,
|
||||
Artifactory = 106,
|
||||
Nexus = 107,
|
||||
GitHubContainerRegistry = 108,
|
||||
GitLabContainerRegistry = 109,
|
||||
|
||||
// SCM providers
|
||||
GitHubApp = 200,
|
||||
GitLabServer = 201,
|
||||
Bitbucket = 202,
|
||||
Gitea = 203,
|
||||
AzureDevOps = 204,
|
||||
|
||||
// CI/CD providers
|
||||
GitHubActions = 300,
|
||||
GitLabCi = 301,
|
||||
Jenkins = 302,
|
||||
CircleCi = 303,
|
||||
AzurePipelines = 304,
|
||||
ArgoWorkflows = 305,
|
||||
Tekton = 306,
|
||||
|
||||
// Repo sources
|
||||
NpmRegistry = 400,
|
||||
PyPi = 401,
|
||||
MavenCentral = 402,
|
||||
NuGetOrg = 403,
|
||||
CratesIo = 404,
|
||||
GoProxy = 405,
|
||||
|
||||
// Runtime hosts
|
||||
EbpfAgent = 500,
|
||||
EtwAgent = 501,
|
||||
DyldInterposer = 502,
|
||||
|
||||
// Feed mirrors
|
||||
StellaOpsMirror = 600,
|
||||
NvdMirror = 601,
|
||||
OsvMirror = 602,
|
||||
|
||||
// Generic / testing
|
||||
InMemory = 900,
|
||||
Custom = 999
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lifecycle status of an integration instance.
|
||||
/// </summary>
|
||||
public enum IntegrationStatus
|
||||
{
|
||||
/// <summary>Just created, not yet tested.</summary>
|
||||
Pending = 0,
|
||||
|
||||
/// <summary>Connection test passed.</summary>
|
||||
Active = 1,
|
||||
|
||||
/// <summary>Connection test failed.</summary>
|
||||
Failed = 2,
|
||||
|
||||
/// <summary>Administratively disabled.</summary>
|
||||
Disabled = 3,
|
||||
|
||||
/// <summary>Marked for deletion.</summary>
|
||||
Archived = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Health check result status.
|
||||
/// </summary>
|
||||
public enum HealthStatus
|
||||
{
|
||||
Unknown = 0,
|
||||
Healthy = 1,
|
||||
Degraded = 2,
|
||||
Unhealthy = 3
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace StellaOps.Integrations.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration passed to connector plugins for test-connection and health checks.
|
||||
/// </summary>
|
||||
public sealed record IntegrationConfig(
|
||||
Guid IntegrationId,
|
||||
IntegrationType Type,
|
||||
IntegrationProvider Provider,
|
||||
string Endpoint,
|
||||
string? ResolvedSecret,
|
||||
string? OrganizationId,
|
||||
IReadOnlyDictionary<string, object>? ExtendedConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Result of a test-connection operation.
|
||||
/// </summary>
|
||||
public sealed record TestConnectionResult(
|
||||
bool Success,
|
||||
string? Message,
|
||||
IReadOnlyDictionary<string, string>? Details,
|
||||
TimeSpan Duration);
|
||||
|
||||
/// <summary>
|
||||
/// Result of a health check operation.
|
||||
/// </summary>
|
||||
public sealed record HealthCheckResult(
|
||||
HealthStatus Status,
|
||||
string? Message,
|
||||
IReadOnlyDictionary<string, string>? Details,
|
||||
DateTimeOffset CheckedAt,
|
||||
TimeSpan Duration);
|
||||
|
||||
/// <summary>
|
||||
/// Integration lifecycle events for downstream consumers.
|
||||
/// </summary>
|
||||
public abstract record IntegrationEvent(Guid IntegrationId, DateTimeOffset OccurredAt);
|
||||
|
||||
public sealed record IntegrationCreatedEvent(
|
||||
Guid IntegrationId,
|
||||
string Name,
|
||||
IntegrationType Type,
|
||||
IntegrationProvider Provider,
|
||||
string? CreatedBy,
|
||||
DateTimeOffset OccurredAt) : IntegrationEvent(IntegrationId, OccurredAt);
|
||||
|
||||
public sealed record IntegrationUpdatedEvent(
|
||||
Guid IntegrationId,
|
||||
string Name,
|
||||
string? UpdatedBy,
|
||||
DateTimeOffset OccurredAt) : IntegrationEvent(IntegrationId, OccurredAt);
|
||||
|
||||
public sealed record IntegrationDeletedEvent(
|
||||
Guid IntegrationId,
|
||||
string? DeletedBy,
|
||||
DateTimeOffset OccurredAt) : IntegrationEvent(IntegrationId, OccurredAt);
|
||||
|
||||
public sealed record IntegrationStatusChangedEvent(
|
||||
Guid IntegrationId,
|
||||
IntegrationStatus OldStatus,
|
||||
IntegrationStatus NewStatus,
|
||||
DateTimeOffset OccurredAt) : IntegrationEvent(IntegrationId, OccurredAt);
|
||||
|
||||
public sealed record IntegrationHealthChangedEvent(
|
||||
Guid IntegrationId,
|
||||
HealthStatus OldHealth,
|
||||
HealthStatus NewHealth,
|
||||
DateTimeOffset OccurredAt) : IntegrationEvent(IntegrationId, OccurredAt);
|
||||
|
||||
public sealed record IntegrationTestConnectionEvent(
|
||||
Guid IntegrationId,
|
||||
bool Success,
|
||||
string? Message,
|
||||
DateTimeOffset OccurredAt) : IntegrationEvent(IntegrationId, OccurredAt);
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>StellaOps.Integrations.Core</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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);
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user