From a7518c4d3b5aa7b1b4cfab0ea17fa1e6ae23d741 Mon Sep 17 00:00:00 2001 From: Yorick de Wid Date: Thu, 25 Apr 2024 12:56:46 +0200 Subject: [PATCH] Add district, municipality, and state repositories --- .../Repositories/IDistrictRepository.cs | 37 +++ .../Repositories/IMunicipalityRepository.cs | 44 ++++ .../Repositories/IStateRepository.cs | 51 ++++ .../Services/GeocoderTranslation.cs | 38 ++- ...nderMapsDataServiceCollectionExtensions.cs | 3 + .../Repositories/DistrictRepository.cs | 186 ++++++++++++++ .../Repositories/MunicipalityRepository.cs | 213 ++++++++++++++++ .../Repositories/StateRepository.cs | 234 ++++++++++++++++++ .../Controllers/GeocoderController.cs | 46 ++++ .../FunderMapsWebApplicationFactory.cs | 2 +- 10 files changed, 852 insertions(+), 2 deletions(-) create mode 100644 src/FunderMaps.Core/Interfaces/Repositories/IDistrictRepository.cs create mode 100644 src/FunderMaps.Core/Interfaces/Repositories/IMunicipalityRepository.cs create mode 100644 src/FunderMaps.Core/Interfaces/Repositories/IStateRepository.cs create mode 100644 src/FunderMaps.Data/Repositories/DistrictRepository.cs create mode 100644 src/FunderMaps.Data/Repositories/MunicipalityRepository.cs create mode 100644 src/FunderMaps.Data/Repositories/StateRepository.cs diff --git a/src/FunderMaps.Core/Interfaces/Repositories/IDistrictRepository.cs b/src/FunderMaps.Core/Interfaces/Repositories/IDistrictRepository.cs new file mode 100644 index 00000000..e4d69541 --- /dev/null +++ b/src/FunderMaps.Core/Interfaces/Repositories/IDistrictRepository.cs @@ -0,0 +1,37 @@ +using FunderMaps.Core.Entities; + +namespace FunderMaps.Core.Interfaces.Repositories; + +/// +/// District repository. +/// +public interface IDistrictRepository : IAsyncRepository +{ + /// + /// Get district by external identifier. + /// + /// External identifier. + /// A single district. + Task GetByExternalIdAsync(string id); + + /// + /// Get district by external address id. + /// + /// External address identifier. + /// A single district. + Task GetByExternalAddressIdAsync(string id); + + /// + /// Get district by external building id. + /// + /// External address identifier. + /// A single district. + Task GetByExternalBuildingIdAsync(string id); + + /// + /// Get district by external neighborhood id. + /// + /// External neighborhood identifier. + /// A single district. + Task GetByExternalNeighborhoodIdAsync(string id); +} diff --git a/src/FunderMaps.Core/Interfaces/Repositories/IMunicipalityRepository.cs b/src/FunderMaps.Core/Interfaces/Repositories/IMunicipalityRepository.cs new file mode 100644 index 00000000..c932d819 --- /dev/null +++ b/src/FunderMaps.Core/Interfaces/Repositories/IMunicipalityRepository.cs @@ -0,0 +1,44 @@ +using FunderMaps.Core.Entities; + +namespace FunderMaps.Core.Interfaces.Repositories; + +/// +/// Municipality repository. +/// +public interface IMunicipalityRepository : IAsyncRepository +{ + /// + /// Get municipality by external identifier. + /// + /// External identifier. + /// A single municipality. + Task GetByExternalIdAsync(string id); + + /// + /// Get municipality by external address id. + /// + /// External address identifier. + /// A single municipality. + Task GetByExternalAddressIdAsync(string id); + + /// + /// Get municipality by external building id. + /// + /// External address identifier. + /// A single municipality. + Task GetByExternalBuildingIdAsync(string id); + + /// + /// Get municipality by external neighborhood id. + /// + /// External neighborhood identifier. + /// A single municipality. + Task GetByExternalNeighborhoodIdAsync(string id); + + /// + /// Get municipality by external district id. + /// + /// External district identifier. + /// A single municipality. + Task GetByExternalDistrictIdAsync(string id); +} diff --git a/src/FunderMaps.Core/Interfaces/Repositories/IStateRepository.cs b/src/FunderMaps.Core/Interfaces/Repositories/IStateRepository.cs new file mode 100644 index 00000000..959596c3 --- /dev/null +++ b/src/FunderMaps.Core/Interfaces/Repositories/IStateRepository.cs @@ -0,0 +1,51 @@ +using FunderMaps.Core.Entities; + +namespace FunderMaps.Core.Interfaces.Repositories; + +/// +/// State repository. +/// +public interface IStateRepository : IAsyncRepository +{ + /// + /// Get state by external identifier. + /// + /// External identifier. + /// A single state. + Task GetByExternalIdAsync(string id); + + /// + /// Get state by external address id. + /// + /// External address identifier. + /// A single state. + Task GetByExternalAddressIdAsync(string id); + + /// + /// Get state by external building id. + /// + /// External address identifier. + /// A single state. + Task GetByExternalBuildingIdAsync(string id); + + /// + /// Get state by external neighborhood id. + /// + /// External neighborhood identifier. + /// A single state. + Task GetByExternalNeighborhoodIdAsync(string id); + + /// + /// Get state by external district id. + /// + /// External district identifier. + /// A single state. + Task GetByExternalDistrictIdAsync(string id); + + /// + /// Get state by external municipality id. + /// + /// External municipality identifier. + /// A single state. + Task GetByExternalMunicipalityIdAsync(string id); +} diff --git a/src/FunderMaps.Core/Services/GeocoderTranslation.cs b/src/FunderMaps.Core/Services/GeocoderTranslation.cs index c1a760b5..bab808d7 100644 --- a/src/FunderMaps.Core/Services/GeocoderTranslation.cs +++ b/src/FunderMaps.Core/Services/GeocoderTranslation.cs @@ -11,7 +11,10 @@ namespace FunderMaps.Core.Services; public class GeocoderTranslation( IAddressRepository addressRepository, IBuildingRepository buildingRepository, - INeighborhoodRepository neighborhoodRepository) + INeighborhoodRepository neighborhoodRepository, + IDistrictRepository districtRepository, + IMunicipalityRepository municipalityRepository, + IStateRepository stateRepository) { /// /// Identify the geocoder datasource from the input. @@ -158,4 +161,37 @@ private static GeocoderDatasource FromIdentifier(string input, out string output GeocoderDatasource.NlCbsNeighborhood => await neighborhoodRepository.GetByExternalIdAsync(id), _ => throw new EntityNotFoundException("Requested neighborhood entity could not be found."), }; + + public async Task GetDistrictIdAsync(string input) => FromIdentifier(input, out string id) switch + { + GeocoderDatasource.FunderMaps => await districtRepository.GetByIdAsync(id), + GeocoderDatasource.NlBagAddress => await districtRepository.GetByExternalAddressIdAsync(id), + GeocoderDatasource.NlBagBuilding => await districtRepository.GetByExternalBuildingIdAsync(id), + GeocoderDatasource.NlCbsNeighborhood => await districtRepository.GetByExternalNeighborhoodIdAsync(id), + GeocoderDatasource.NlCbsDistrict => await districtRepository.GetByExternalIdAsync(id), + _ => throw new EntityNotFoundException("Requested district entity could not be found."), + }; + + public async Task GetMunicipalityIdAsync(string input) => FromIdentifier(input, out string id) switch + { + GeocoderDatasource.FunderMaps => await municipalityRepository.GetByIdAsync(id), + GeocoderDatasource.NlBagAddress => await municipalityRepository.GetByExternalAddressIdAsync(id), + GeocoderDatasource.NlBagBuilding => await municipalityRepository.GetByExternalBuildingIdAsync(id), + GeocoderDatasource.NlCbsNeighborhood => await municipalityRepository.GetByExternalNeighborhoodIdAsync(id), + GeocoderDatasource.NlCbsDistrict => await municipalityRepository.GetByExternalDistrictIdAsync(id), + GeocoderDatasource.NlCbsMunicipality => await municipalityRepository.GetByExternalIdAsync(id), + _ => throw new EntityNotFoundException("Requested municipality entity could not be found."), + }; + + public async Task GetStateIdAsync(string input) => FromIdentifier(input, out string id) switch + { + GeocoderDatasource.FunderMaps => await stateRepository.GetByIdAsync(id), + GeocoderDatasource.NlBagAddress => await stateRepository.GetByExternalAddressIdAsync(id), + GeocoderDatasource.NlBagBuilding => await stateRepository.GetByExternalBuildingIdAsync(id), + GeocoderDatasource.NlCbsNeighborhood => await stateRepository.GetByExternalNeighborhoodIdAsync(id), + GeocoderDatasource.NlCbsDistrict => await stateRepository.GetByExternalDistrictIdAsync(id), + GeocoderDatasource.NlCbsMunicipality => await stateRepository.GetByExternalMunicipalityIdAsync(id), + GeocoderDatasource.NlCbsState => await stateRepository.GetByExternalIdAsync(id), + _ => throw new EntityNotFoundException("Requested state entity could not be found."), + }; } diff --git a/src/FunderMaps.Data/Extensions/FunderMapsDataServiceCollectionExtensions.cs b/src/FunderMaps.Data/Extensions/FunderMapsDataServiceCollectionExtensions.cs index 33070f45..a8c52e8b 100644 --- a/src/FunderMaps.Data/Extensions/FunderMapsDataServiceCollectionExtensions.cs +++ b/src/FunderMaps.Data/Extensions/FunderMapsDataServiceCollectionExtensions.cs @@ -62,17 +62,20 @@ public static IServiceCollection AddFunderMapsDataServices(this IServiceCollecti services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); + services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); + services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); + services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); services.AddContextRepository(); diff --git a/src/FunderMaps.Data/Repositories/DistrictRepository.cs b/src/FunderMaps.Data/Repositories/DistrictRepository.cs new file mode 100644 index 00000000..16a4f6bc --- /dev/null +++ b/src/FunderMaps.Data/Repositories/DistrictRepository.cs @@ -0,0 +1,186 @@ +using Dapper; +using FunderMaps.Core; +using FunderMaps.Core.Entities; +using FunderMaps.Core.Exceptions; +using FunderMaps.Core.Interfaces.Repositories; + +namespace FunderMaps.Data.Repositories; + +/// +/// District repository. +/// +internal class DistrictRepository : RepositoryBase, IDistrictRepository +{ + /// + /// Retrieve number of entities. + /// + /// Number of entities. + public override async Task CountAsync() + { + var sql = @" + SELECT COUNT(*) + FROM geocoder.district"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + return await connection.ExecuteScalarAsync(sql); + } + + public async Task GetByExternalIdAsync(string id) + { + if (TryGetEntity(id, out District? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- District + d.id, + d.name, + d.water, + d.external_id, + d.municipality_id + FROM geocoder.district AS d + WHERE d.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var district = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return district is null ? throw new EntityNotFoundException(nameof(District)) : CacheEntity(district); + } + + public async Task GetByExternalAddressIdAsync(string id) + { + if (TryGetEntity(id, out District? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- District + d.id, + d.name, + d.water, + d.external_id, + d.municipality_id + FROM geocoder.address AS a + JOIN geocoder.address_building AS ab ON ab.address_id = a.id + JOIN geocoder.building_active AS ba ON ba.id = ab.building_id + JOIN geocoder.neighborhood AS n ON n.id = ba.neighborhood_id + JOIN geocoder.district d ON d.id = n.district_id + WHERE a.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var district = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return district is null ? throw new EntityNotFoundException(nameof(District)) : CacheEntity(district); + } + + public async Task GetByExternalBuildingIdAsync(string id) + { + if (TryGetEntity(id, out District? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- District + d.id, + d.name, + d.water, + d.external_id, + d.municipality_id + FROM geocoder.building_active AS ba + JOIN geocoder.neighborhood AS n on n.id = ba.neighborhood_id + JOIN geocoder.district d ON d.id = n.district_id + WHERE ba.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var district = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return district is null ? throw new EntityNotFoundException(nameof(District)) : CacheEntity(district); + } + + public async Task GetByExternalNeighborhoodIdAsync(string id) + { + if (TryGetEntity(id, out District? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- District + d.id, + d.name, + d.water, + d.external_id, + d.municipality_id + FROM geocoder.neighborhood AS n + JOIN geocoder.district d ON d.id = n.district_id + WHERE n.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var district = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return district is null ? throw new EntityNotFoundException(nameof(District)) : CacheEntity(district); + } + + + /// + /// Retrieve by id. + /// + /// Unique identifier. + /// . + public override async Task GetByIdAsync(string id) + { + if (TryGetEntity(id, out District? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- District + d.id, + d.name, + d.water, + d.external_id, + d.municipality_id + FROM geocoder.district AS d + WHERE d.id = @id + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var district = await connection.QuerySingleOrDefaultAsync(sql, new { id }); + return district is null ? throw new EntityNotFoundException(nameof(District)) : CacheEntity(district); + } + + /// + /// Retrieve all . + /// + /// List of . + public override async IAsyncEnumerable ListAllAsync(Navigation navigation) + { + var sql = @" + SELECT -- District + d.id, + d.name, + d.water, + d.external_id, + d.municipality_id + FROM geocoder.district AS d + OFFSET @offset + LIMIT @limit"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + await foreach (var item in connection.QueryUnbufferedAsync(sql, navigation)) + { + yield return CacheEntity(item); + } + } +} diff --git a/src/FunderMaps.Data/Repositories/MunicipalityRepository.cs b/src/FunderMaps.Data/Repositories/MunicipalityRepository.cs new file mode 100644 index 00000000..580c8ddd --- /dev/null +++ b/src/FunderMaps.Data/Repositories/MunicipalityRepository.cs @@ -0,0 +1,213 @@ +using Dapper; +using FunderMaps.Core; +using FunderMaps.Core.Entities; +using FunderMaps.Core.Exceptions; +using FunderMaps.Core.Interfaces.Repositories; + +namespace FunderMaps.Data.Repositories; + +/// +/// Municipality repository. +/// +internal class MunicipalityRepository : RepositoryBase, IMunicipalityRepository +{ + /// + /// Retrieve number of entities. + /// + /// Number of entities. + public override async Task CountAsync() + { + var sql = @" + SELECT COUNT(*) + FROM geocoder.municipality"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + return await connection.ExecuteScalarAsync(sql); + } + + public async Task GetByExternalIdAsync(string id) + { + if (TryGetEntity(id, out Municipality? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- Municipality + m.id, + m.name, + m.water, + m.external_id, + m.state_id + FROM geocoder.municipality AS m + WHERE m.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var municipality = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return municipality is null ? throw new EntityNotFoundException(nameof(Municipality)) : CacheEntity(municipality); + } + + public async Task GetByExternalAddressIdAsync(string id) + { + if (TryGetEntity(id, out Municipality? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- Municipality + m.id, + m.name, + m.water, + m.external_id, + m.state_id + FROM geocoder.address AS a + JOIN geocoder.address_building AS ab ON ab.address_id = a.id + JOIN geocoder.building_active AS ba ON ba.id = ab.building_id + JOIN geocoder.neighborhood AS n ON n.id = ba.neighborhood_id + JOIN geocoder.district d ON d.id = n.district_id + JOIN geocoder.municipality m ON m.id = d.municipality_id + WHERE a.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var municipality = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return municipality is null ? throw new EntityNotFoundException(nameof(Municipality)) : CacheEntity(municipality); + } + + public async Task GetByExternalBuildingIdAsync(string id) + { + if (TryGetEntity(id, out Municipality? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- Municipality + m.id, + m.name, + m.water, + m.external_id, + m.state_id + FROM geocoder.building_active AS ba + JOIN geocoder.neighborhood AS n on n.id = ba.neighborhood_id + JOIN geocoder.district d ON d.id = n.district_id + JOIN geocoder.municipality m ON m.id = d.municipality_id + WHERE ba.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var municipality = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return municipality is null ? throw new EntityNotFoundException(nameof(Municipality)) : CacheEntity(municipality); + } + + public async Task GetByExternalNeighborhoodIdAsync(string id) + { + if (TryGetEntity(id, out Municipality? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- Municipality + m.id, + m.name, + m.water, + m.external_id, + m.state_id + FROM geocoder.neighborhood AS n + JOIN geocoder.district d ON d.id = n.district_id + JOIN geocoder.municipality m ON m.id = d.municipality_id + WHERE n.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var municipality = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return municipality is null ? throw new EntityNotFoundException(nameof(Municipality)) : CacheEntity(municipality); + } + + public async Task GetByExternalDistrictIdAsync(string id) + { + if (TryGetEntity(id, out Municipality? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- Municipality + m.id, + m.name, + m.water, + m.external_id, + m.state_id + FROM geocoder.district d + JOIN geocoder.municipality m ON m.id = d.municipality_id + WHERE d.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var municipality = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return municipality is null ? throw new EntityNotFoundException(nameof(Municipality)) : CacheEntity(municipality); + } + + /// + /// Retrieve by id. + /// + /// Unique identifier. + /// . + public override async Task GetByIdAsync(string id) + { + if (TryGetEntity(id, out Municipality? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- Municipality + m.id, + m.name, + m.water, + m.external_id, + m.state_id + FROM geocoder.municipality AS m + WHERE m.id = @id + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var municipality = await connection.QuerySingleOrDefaultAsync(sql, new { id }); + return municipality is null ? throw new EntityNotFoundException(nameof(Municipality)) : CacheEntity(municipality); + } + + /// + /// Retrieve all . + /// + /// List of . + public override async IAsyncEnumerable ListAllAsync(Navigation navigation) + { + var sql = @" + SELECT -- Municipality + m.id, + m.name, + m.water, + m.external_id, + m.state_id + FROM geocoder.municipality AS m + OFFSET @offset + LIMIT @limit"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + await foreach (var item in connection.QueryUnbufferedAsync(sql, navigation)) + { + yield return CacheEntity(item); + } + } +} diff --git a/src/FunderMaps.Data/Repositories/StateRepository.cs b/src/FunderMaps.Data/Repositories/StateRepository.cs new file mode 100644 index 00000000..fbfa2704 --- /dev/null +++ b/src/FunderMaps.Data/Repositories/StateRepository.cs @@ -0,0 +1,234 @@ +using Dapper; +using FunderMaps.Core; +using FunderMaps.Core.Entities; +using FunderMaps.Core.Exceptions; +using FunderMaps.Core.Interfaces.Repositories; + +namespace FunderMaps.Data.Repositories; + +/// +/// State repository. +/// +internal class StateRepository : RepositoryBase, IStateRepository +{ + /// + /// Retrieve number of entities. + /// + /// Number of entities. + public override async Task CountAsync() + { + var sql = @" + SELECT COUNT(*) + FROM geocoder.state"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + return await connection.ExecuteScalarAsync(sql); + } + + public async Task GetByExternalIdAsync(string id) + { + if (TryGetEntity(id, out State? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.state AS s + WHERE s.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var state = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return state is null ? throw new EntityNotFoundException(nameof(State)) : CacheEntity(state); + } + + public async Task GetByExternalAddressIdAsync(string id) + { + if (TryGetEntity(id, out State? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.address AS a + JOIN geocoder.address_building AS ab ON ab.address_id = a.id + JOIN geocoder.building_active AS ba ON ba.id = ab.building_id + JOIN geocoder.neighborhood AS n ON n.id = ba.neighborhood_id + JOIN geocoder.district d ON d.id = n.district_id + JOIN geocoder.municipality m ON m.id = d.municipality_id + JOIN geocoder.state s ON s.id = m.state_id + WHERE a.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var state = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return state is null ? throw new EntityNotFoundException(nameof(State)) : CacheEntity(state); + } + + public async Task GetByExternalBuildingIdAsync(string id) + { + if (TryGetEntity(id, out State? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.building_active AS ba + JOIN geocoder.neighborhood AS n on n.id = ba.neighborhood_id + JOIN geocoder.district d ON d.id = n.district_id + JOIN geocoder.municipality m ON m.id = d.municipality_id + JOIN geocoder.state s ON s.id = m.state_id + WHERE ba.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var state = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return state is null ? throw new EntityNotFoundException(nameof(State)) : CacheEntity(state); + } + + public async Task GetByExternalNeighborhoodIdAsync(string id) + { + if (TryGetEntity(id, out State? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.neighborhood AS n + JOIN geocoder.district d ON d.id = n.district_id + JOIN geocoder.municipality m ON m.id = d.municipality_id + JOIN geocoder.state s ON s.id = m.state_id + WHERE n.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var state = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return state is null ? throw new EntityNotFoundException(nameof(State)) : CacheEntity(state); + } + + public async Task GetByExternalDistrictIdAsync(string id) + { + if (TryGetEntity(id, out State? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.district d + JOIN geocoder.municipality m ON m.id = d.municipality_id + JOIN geocoder.state s ON s.id = m.state_id + WHERE d.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var state = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return state is null ? throw new EntityNotFoundException(nameof(State)) : CacheEntity(state); + } + + public async Task GetByExternalMunicipalityIdAsync(string id) + { + if (TryGetEntity(id, out State? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.municipality m + JOIN geocoder.state s ON s.id = m.state_id + WHERE m.external_id = upper(@external_id) + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var state = await connection.QuerySingleOrDefaultAsync(sql, new { external_id = id }); + return state is null ? throw new EntityNotFoundException(nameof(State)) : CacheEntity(state); + } + + /// + /// Retrieve by id. + /// + /// Unique identifier. + /// . + public override async Task GetByIdAsync(string id) + { + if (TryGetEntity(id, out State? entity)) + { + return entity ?? throw new InvalidOperationException(); + } + + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.state AS s + WHERE s.id = @id + LIMIT 1"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + var state = await connection.QuerySingleOrDefaultAsync(sql, new { id }); + return state is null ? throw new EntityNotFoundException(nameof(State)) : CacheEntity(state); + } + + /// + /// Retrieve all . + /// + /// List of . + public override async IAsyncEnumerable ListAllAsync(Navigation navigation) + { + var sql = @" + SELECT -- State + s.id, + s.name, + s.water, + s.external_id + FROM geocoder.state AS s + OFFSET @offset + LIMIT @limit"; + + await using var connection = DbContextFactory.DbProvider.ConnectionScope(); + + await foreach (var item in connection.QueryUnbufferedAsync(sql, navigation)) + { + yield return CacheEntity(item); + } + } +} diff --git a/src/FunderMaps.WebApi/Controllers/GeocoderController.cs b/src/FunderMaps.WebApi/Controllers/GeocoderController.cs index 53571397..14961c3e 100644 --- a/src/FunderMaps.WebApi/Controllers/GeocoderController.cs +++ b/src/FunderMaps.WebApi/Controllers/GeocoderController.cs @@ -54,6 +54,39 @@ public Task GetBuildingAsync(string id) public Task GetNeighborhoodAsync(string id) => geocoderTranslation.GetNeighborhoodIdAsync(id); + // GET: api/geocoder/district/{id} + /// + /// Get district by identifier. + /// + /// + /// Cache response for 8 hours. District will not change often. + /// + [HttpGet("district/{id}"), ResponseCache(Duration = 60 * 60 * 12)] + public Task GetDistrictAsync(string id) + => geocoderTranslation.GetDistrictIdAsync(id); + + // GET: api/geocoder/municipality/{id} + /// + /// Get municipality by identifier. + /// + /// + /// Cache response for 8 hours. Municipality will not change often. + /// + [HttpGet("municipality/{id}"), ResponseCache(Duration = 60 * 60 * 12)] + public Task GetMunicipalityAsync(string id) + => geocoderTranslation.GetMunicipalityIdAsync(id); + + // GET: api/geocoder/state/{id} + /// + /// Get state by identifier. + /// + /// + /// Cache response for 8 hours. State will not change often. + /// + [HttpGet("state/{id}"), ResponseCache(Duration = 60 * 60 * 12)] + public Task GetStateAsync(string id) + => geocoderTranslation.GetStateIdAsync(id); + // GET: api/geocoder/{id} /// /// Get geocoder information by identifier. @@ -69,12 +102,25 @@ public async Task GetAsync(string id) var neighborhood = building.NeighborhoodId is not null ? await geocoderTranslation.GetNeighborhoodIdAsync(building.NeighborhoodId) : null; + // TODO: Get district from geocoderTranslation + var district = neighborhood!.DistrictId is not null + ? await geocoderTranslation.GetDistrictIdAsync(neighborhood.DistrictId) + : null; + var municipality = district!.MunicipalityId is not null + ? await geocoderTranslation.GetMunicipalityIdAsync(district.MunicipalityId) + : null; + var state = municipality!.StateId is not null + ? await geocoderTranslation.GetStateIdAsync(municipality.StateId) + : null; return new GeocoderInfo { Building = building, Address = address, Neighborhood = neighborhood, + District = district, + Municipality = municipality, + State = state, }; } } diff --git a/tests/FunderMaps.Webservice.Tests/FunderMapsWebApplicationFactory.cs b/tests/FunderMaps.Webservice.Tests/FunderMapsWebApplicationFactory.cs index 3a8db98e..5ada8907 100644 --- a/tests/FunderMaps.Webservice.Tests/FunderMapsWebApplicationFactory.cs +++ b/tests/FunderMaps.Webservice.Tests/FunderMapsWebApplicationFactory.cs @@ -255,7 +255,7 @@ public async Task ResetResetKey(Guid id) { await Task.CompletedTask; - throw new NotImplementedException(); + // TODO: Implement. } public async Task RegisterAccess(Guid id)