feat(api): UDT-004 dominio + repositorio + application roles (tdd)
- Migraciones V003 (tabla Rol + 8 seeds canonicos) y V004 (drop CK + FK Usuario.Rol) - Dominio: Rol entity + 3 excepciones (RolNotFound/AlreadyExists/InUse) - Infraestructura: RolRepository (Dapper) con List/Get/ExistsActive/Add/Update/HasActiveUsuarios - Application: CRUD queries y commands (List, Get, Create, Update, Deactivate) + validators (codigo regex ^[a-z][a-z0-9_]*$) - Validator UDT-003: whitelist alineada a codigos canonicos (full IRolRepository lookup diferido a Phase 5.1) - Tests: 169 application + 15 api (todos verdes). Respawn configurado para re-seedear Rol canonical post-reset. - Estricto TDD: RED/GREEN/TRIANGULATE en todos los handlers nuevos.
This commit is contained in:
129
src/api/SIGCM2.Infrastructure/Persistence/RolRepository.cs
Normal file
129
src/api/SIGCM2.Infrastructure/Persistence/RolRepository.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Dapper;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Domain.Entities;
|
||||
|
||||
namespace SIGCM2.Infrastructure.Persistence;
|
||||
|
||||
public sealed class RolRepository : IRolRepository
|
||||
{
|
||||
private readonly SqlConnectionFactory _connectionFactory;
|
||||
|
||||
public RolRepository(SqlConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Rol>> ListAsync(CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT Id, Codigo, Nombre, Descripcion, Activo, FechaCreacion, FechaModificacion
|
||||
FROM dbo.Rol
|
||||
ORDER BY Id
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var rows = await connection.QueryAsync<RolRow>(sql);
|
||||
return rows.Select(MapRow).ToList();
|
||||
}
|
||||
|
||||
public async Task<Rol?> GetByCodigoAsync(string codigo, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT Id, Codigo, Nombre, Descripcion, Activo, FechaCreacion, FechaModificacion
|
||||
FROM dbo.Rol
|
||||
WHERE Codigo = @Codigo
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var row = await connection.QuerySingleOrDefaultAsync<RolRow>(sql, new { Codigo = codigo });
|
||||
return row is null ? null : MapRow(row);
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsActiveByCodigoAsync(string codigo, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT COUNT(1) FROM dbo.Rol WHERE Codigo = @Codigo AND Activo = 1
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var count = await connection.ExecuteScalarAsync<int>(sql, new { Codigo = codigo });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
public async Task<int> AddAsync(Rol rol, CancellationToken ct = default)
|
||||
{
|
||||
// DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()).
|
||||
const string sql = """
|
||||
INSERT INTO dbo.Rol (Codigo, Nombre, Descripcion)
|
||||
OUTPUT INSERTED.Id
|
||||
VALUES (@Codigo, @Nombre, @Descripcion)
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
return await connection.ExecuteScalarAsync<int>(sql, new
|
||||
{
|
||||
rol.Codigo,
|
||||
rol.Nombre,
|
||||
rol.Descripcion,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(string codigo, string nombre, string? descripcion, bool activo, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
UPDATE dbo.Rol
|
||||
SET Nombre = @Nombre,
|
||||
Descripcion = @Descripcion,
|
||||
Activo = @Activo,
|
||||
FechaModificacion = SYSUTCDATETIME()
|
||||
WHERE Codigo = @Codigo;
|
||||
SELECT @@ROWCOUNT;
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var rows = await connection.ExecuteScalarAsync<int>(sql, new { Codigo = codigo, Nombre = nombre, Descripcion = descripcion, Activo = activo });
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
public async Task<bool> HasActiveUsuariosAsync(string codigo, CancellationToken ct = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT COUNT(1) FROM dbo.Usuario WHERE Rol = @Codigo AND Activo = 1
|
||||
""";
|
||||
|
||||
await using var connection = _connectionFactory.CreateConnection();
|
||||
await connection.OpenAsync(ct);
|
||||
|
||||
var count = await connection.ExecuteScalarAsync<int>(sql, new { Codigo = codigo });
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private static Rol MapRow(RolRow row)
|
||||
=> new(
|
||||
id: row.Id,
|
||||
codigo: row.Codigo,
|
||||
nombre: row.Nombre,
|
||||
descripcion: row.Descripcion,
|
||||
activo: row.Activo,
|
||||
fechaCreacion: row.FechaCreacion,
|
||||
fechaModificacion: row.FechaModificacion);
|
||||
|
||||
private sealed record RolRow(
|
||||
int Id,
|
||||
string Codigo,
|
||||
string Nombre,
|
||||
string? Descripcion,
|
||||
bool Activo,
|
||||
DateTime FechaCreacion,
|
||||
DateTime? FechaModificacion);
|
||||
}
|
||||
Reference in New Issue
Block a user