From f7f0e787b675e21f5a4ab468917e2b1281a5ca61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Sun, 22 Feb 2026 17:14:05 -0600 Subject: [PATCH] feat(thalos-dal): add runtime provider and repository wiring --- .../IdentityDalGrpcContractAdapter.cs | 47 ++++++++++++++ .../ThalosDalServiceCollectionExtensions.cs | 38 +++++++++++ .../Health/DalDependencyHealthCheck.cs | 27 ++++++++ .../InMemory/InMemoryModuleDataProvider.cs | 22 +++++++ .../InMemoryPermissionDataProvider.cs | 23 +++++++ .../InMemory/InMemoryRoleDataProvider.cs | 22 +++++++ .../InMemory/InMemoryTenantDataProvider.cs | 23 +++++++ .../InMemory/InMemoryUserDataProvider.cs | 28 ++++++++ .../Repositories/IdentityRepository.cs | 52 +++++++++++++++ src/Thalos.DAL/Thalos.DAL.csproj | 1 + .../RuntimeWiringTests.cs | 65 +++++++++++++++++++ .../Thalos.DAL.UnitTests.csproj | 1 + 12 files changed, 349 insertions(+) create mode 100644 src/Thalos.DAL/Adapters/IdentityDalGrpcContractAdapter.cs create mode 100644 src/Thalos.DAL/DependencyInjection/ThalosDalServiceCollectionExtensions.cs create mode 100644 src/Thalos.DAL/Health/DalDependencyHealthCheck.cs create mode 100644 src/Thalos.DAL/Providers/InMemory/InMemoryModuleDataProvider.cs create mode 100644 src/Thalos.DAL/Providers/InMemory/InMemoryPermissionDataProvider.cs create mode 100644 src/Thalos.DAL/Providers/InMemory/InMemoryRoleDataProvider.cs create mode 100644 src/Thalos.DAL/Providers/InMemory/InMemoryTenantDataProvider.cs create mode 100644 src/Thalos.DAL/Providers/InMemory/InMemoryUserDataProvider.cs create mode 100644 src/Thalos.DAL/Repositories/IdentityRepository.cs create mode 100644 tests/Thalos.DAL.UnitTests/RuntimeWiringTests.cs diff --git a/src/Thalos.DAL/Adapters/IdentityDalGrpcContractAdapter.cs b/src/Thalos.DAL/Adapters/IdentityDalGrpcContractAdapter.cs new file mode 100644 index 0000000..f07d917 --- /dev/null +++ b/src/Thalos.DAL/Adapters/IdentityDalGrpcContractAdapter.cs @@ -0,0 +1,47 @@ +using Core.Blueprint.Common.Runtime; +using Thalos.DAL.Contracts; +using Thalos.DAL.Grpc; + +namespace Thalos.DAL.Adapters; + +/// +/// Default adapter implementation for DAL gRPC contract translation. +/// +public sealed class IdentityDalGrpcContractAdapter(IBlueprintSystemClock clock) : IIdentityDalGrpcContractAdapter +{ + /// + public IdentityPolicyDalGrpcContract ToGrpcPolicyRequest(IdentityPolicyLookupRequest request) + { + return new IdentityPolicyDalGrpcContract(request.SubjectId, request.TenantId, request.PermissionCode); + } + + /// + public IdentityPolicyLookupRequest FromGrpcPolicyRequest(IdentityPolicyDalGrpcContract contract) + { + return new IdentityPolicyLookupRequest( + CreateEnvelope(), + contract.SubjectId, + contract.TenantId, + contract.PermissionCode); + } + + /// + public IdentityTokenDalGrpcContract ToGrpcTokenRequest(IdentityTokenLookupRequest request) + { + return new IdentityTokenDalGrpcContract(request.SubjectId, request.TenantId); + } + + /// + public IdentityTokenLookupRequest FromGrpcTokenRequest(IdentityTokenDalGrpcContract contract) + { + return new IdentityTokenLookupRequest( + CreateEnvelope(), + contract.SubjectId, + contract.TenantId); + } + + private IdentityContractEnvelope CreateEnvelope() + { + return new IdentityContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + } +} diff --git a/src/Thalos.DAL/DependencyInjection/ThalosDalServiceCollectionExtensions.cs b/src/Thalos.DAL/DependencyInjection/ThalosDalServiceCollectionExtensions.cs new file mode 100644 index 0000000..1bc5e73 --- /dev/null +++ b/src/Thalos.DAL/DependencyInjection/ThalosDalServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +using Core.Blueprint.Common.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Thalos.DAL.Adapters; +using Thalos.DAL.Health; +using Thalos.DAL.Providers; +using Thalos.DAL.Providers.InMemory; +using Thalos.DAL.Repositories; + +namespace Thalos.DAL.DependencyInjection; + +/// +/// Registers thalos dal runtime provider, repository, and adapter implementations. +/// +public static class ThalosDalServiceCollectionExtensions +{ + /// + /// Adds thalos dal runtime implementations aligned with blueprint runtime core. + /// + /// Service collection. + /// Service collection for fluent chaining. + public static IServiceCollection AddThalosDalRuntime(this IServiceCollection services) + { + services.AddBlueprintRuntimeCore(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/Thalos.DAL/Health/DalDependencyHealthCheck.cs b/src/Thalos.DAL/Health/DalDependencyHealthCheck.cs new file mode 100644 index 0000000..e6a9f0e --- /dev/null +++ b/src/Thalos.DAL/Health/DalDependencyHealthCheck.cs @@ -0,0 +1,27 @@ +using Core.Blueprint.Common.Runtime; +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Health; + +/// +/// Default DAL dependency health check implementation. +/// +public sealed class DalDependencyHealthCheck(IBlueprintSystemClock clock) : IDalDependencyHealthCheck +{ + /// + public Task CheckAsync(CancellationToken cancellationToken = default) + { + var envelope = new IdentityContractEnvelope("1.0.0", $"corr-{clock.UtcNow:yyyyMMddHHmmssfff}"); + IReadOnlyList dependencyNames = + [ + "IUserDataProvider", + "IRoleDataProvider", + "IPermissionDataProvider", + "IModuleDataProvider", + "ITenantDataProvider" + ]; + + var status = new DalDependencyHealthStatus(envelope, true, dependencyNames); + return Task.FromResult(status); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryModuleDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryModuleDataProvider.cs new file mode 100644 index 0000000..db80e20 --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryModuleDataProvider.cs @@ -0,0 +1,22 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity module lookup contracts. +/// +public sealed class InMemoryModuleDataProvider : IModuleDataProvider +{ + /// + public Task> ReadModulesAsync( + IdentityModuleLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityModuleRecord(request.Envelope, "identity", true) + ]; + + return Task.FromResult(records); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryPermissionDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryPermissionDataProvider.cs new file mode 100644 index 0000000..5cb7626 --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryPermissionDataProvider.cs @@ -0,0 +1,23 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity permission lookup contracts. +/// +public sealed class InMemoryPermissionDataProvider : IPermissionDataProvider +{ + /// + public Task> ReadPermissionsAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityPermissionRecord(request.Envelope, "identity.token.issue", "identity.admin"), + new IdentityPermissionRecord(request.Envelope, "identity.policy.evaluate", "identity.admin") + ]; + + return Task.FromResult(records); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryRoleDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryRoleDataProvider.cs new file mode 100644 index 0000000..2b7f3ac --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryRoleDataProvider.cs @@ -0,0 +1,22 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity role lookup contracts. +/// +public sealed class InMemoryRoleDataProvider : IRoleDataProvider +{ + /// + public Task> ReadRolesAsync( + IdentityRoleLookupRequest request, + CancellationToken cancellationToken = default) + { + IReadOnlyList records = + [ + new IdentityRoleRecord(request.Envelope, "identity.admin", request.TenantId) + ]; + + return Task.FromResult(records); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryTenantDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryTenantDataProvider.cs new file mode 100644 index 0000000..e61cef3 --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryTenantDataProvider.cs @@ -0,0 +1,23 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity tenant lookup contracts. +/// +public sealed class InMemoryTenantDataProvider : ITenantDataProvider +{ + /// + public Task ReadTenantAsync( + IdentityTenantLookupRequest request, + CancellationToken cancellationToken = default) + { + var record = new IdentityTenantRecord( + request.Envelope, + request.TenantId, + $"tenant-{request.TenantId}", + true); + + return Task.FromResult(record); + } +} diff --git a/src/Thalos.DAL/Providers/InMemory/InMemoryUserDataProvider.cs b/src/Thalos.DAL/Providers/InMemory/InMemoryUserDataProvider.cs new file mode 100644 index 0000000..4d54c23 --- /dev/null +++ b/src/Thalos.DAL/Providers/InMemory/InMemoryUserDataProvider.cs @@ -0,0 +1,28 @@ +using Thalos.DAL.Contracts; + +namespace Thalos.DAL.Providers.InMemory; + +/// +/// In-memory provider for identity user lookup contracts. +/// +public sealed class InMemoryUserDataProvider : IUserDataProvider +{ + /// + public Task ReadUserAsync( + IdentityUserLookupRequest request, + CancellationToken cancellationToken = default) + { + if (request.SubjectId.StartsWith("missing-", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(null); + } + + var record = new IdentityUserRecord( + request.Envelope, + request.SubjectId, + "tenant-default", + "active"); + + return Task.FromResult(record); + } +} diff --git a/src/Thalos.DAL/Repositories/IdentityRepository.cs b/src/Thalos.DAL/Repositories/IdentityRepository.cs new file mode 100644 index 0000000..8725a0d --- /dev/null +++ b/src/Thalos.DAL/Repositories/IdentityRepository.cs @@ -0,0 +1,52 @@ +using Thalos.DAL.Contracts; +using Thalos.DAL.Providers; + +namespace Thalos.DAL.Repositories; + +/// +/// Default identity repository implementation composed from DAL providers. +/// +public sealed class IdentityRepository( + IUserDataProvider userDataProvider, + IPermissionDataProvider permissionDataProvider) : IIdentityRepository +{ + /// + public async Task ReadIdentityTokenAsync( + IdentityTokenLookupRequest request, + CancellationToken cancellationToken = default) + { + var userRequest = new IdentityUserLookupRequest(request.Envelope, request.SubjectId); + var userRecord = await userDataProvider.ReadUserAsync(userRequest, cancellationToken); + if (userRecord is null) + { + return null; + } + + var token = $"{request.SubjectId}:{request.TenantId}:token"; + return new IdentityTokenRecord(request.Envelope, request.SubjectId, request.TenantId, token, 1800); + } + + /// + public async Task ReadIdentityPolicyAsync( + IdentityPolicyLookupRequest request, + CancellationToken cancellationToken = default) + { + var userRequest = new IdentityUserLookupRequest(request.Envelope, request.SubjectId); + var userRecord = await userDataProvider.ReadUserAsync(userRequest, cancellationToken); + if (userRecord is null) + { + return null; + } + + var contextSatisfied = string.Equals(userRecord.Status, "active", StringComparison.OrdinalIgnoreCase); + return new IdentityPolicyRecord(request.Envelope, request.SubjectId, request.PermissionCode, contextSatisfied); + } + + /// + public Task> ReadPermissionSetAsync( + IdentityPermissionSetLookupRequest request, + CancellationToken cancellationToken = default) + { + return permissionDataProvider.ReadPermissionsAsync(request, cancellationToken); + } +} diff --git a/src/Thalos.DAL/Thalos.DAL.csproj b/src/Thalos.DAL/Thalos.DAL.csproj index 04a4bbc..bf5537a 100644 --- a/src/Thalos.DAL/Thalos.DAL.csproj +++ b/src/Thalos.DAL/Thalos.DAL.csproj @@ -5,6 +5,7 @@ enable + diff --git a/tests/Thalos.DAL.UnitTests/RuntimeWiringTests.cs b/tests/Thalos.DAL.UnitTests/RuntimeWiringTests.cs new file mode 100644 index 0000000..6c902fc --- /dev/null +++ b/tests/Thalos.DAL.UnitTests/RuntimeWiringTests.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.DependencyInjection; +using Thalos.DAL.Adapters; +using Thalos.DAL.Contracts; +using Thalos.DAL.DependencyInjection; +using Thalos.DAL.Health; +using Thalos.DAL.Repositories; + +namespace Thalos.DAL.UnitTests; + +public class RuntimeWiringTests +{ + [Fact] + public async Task AddThalosDalRuntime_WhenResolved_WiresRepositoryAndProviders() + { + var services = new ServiceCollection(); + services.AddThalosDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var repository = provider.GetRequiredService(); + var request = new IdentityTokenLookupRequest( + new IdentityContractEnvelope("1.0.0", "corr-123"), + "user-1", + "tenant-1"); + + var response = await repository.ReadIdentityTokenAsync(request); + + Assert.NotNull(response); + Assert.Equal("user-1", response.SubjectId); + Assert.Equal("tenant-1", response.TenantId); + Assert.Equal(1800, response.ExpiresInSeconds); + } + + [Fact] + public void AddThalosDalRuntime_WhenResolved_WiresGrpcContractAdapter() + { + var services = new ServiceCollection(); + services.AddThalosDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var adapter = provider.GetRequiredService(); + var grpcContract = new Thalos.DAL.Grpc.IdentityTokenDalGrpcContract("user-2", "tenant-2"); + + var request = adapter.FromGrpcTokenRequest(grpcContract); + + Assert.Equal("user-2", request.SubjectId); + Assert.Equal("tenant-2", request.TenantId); + Assert.NotEmpty(request.Envelope.CorrelationId); + } + + [Fact] + public async Task AddThalosDalRuntime_WhenResolved_WiresDependencyHealthCheck() + { + var services = new ServiceCollection(); + services.AddThalosDalRuntime(); + + using var provider = services.BuildServiceProvider(); + var healthCheck = provider.GetRequiredService(); + + var status = await healthCheck.CheckAsync(); + + Assert.True(status.IsHealthy); + Assert.Contains("IUserDataProvider", status.DependencyNames); + Assert.Contains("IPermissionDataProvider", status.DependencyNames); + } +} diff --git a/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj b/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj index 202abae..8f4a236 100644 --- a/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj +++ b/tests/Thalos.DAL.UnitTests/Thalos.DAL.UnitTests.csproj @@ -7,6 +7,7 @@ +