From c27fce37c67561477f2eddfac9a436038715ad27 Mon Sep 17 00:00:00 2001 From: Eugene Fox Date: Fri, 23 Feb 2024 13:04:06 +0000 Subject: [PATCH] - Added /Import endpoints - Minor refactoring and improved validation --- .../Controllers/PointsController.cs | 30 ++++++++++ .../Controllers/TracksController.cs | 23 ++++++++ .../Track/UpsertTrackRequest.cs | 4 +- .../Repositories/IPointRepository.cs | 7 +++ .../Repositories/ITrackRepository.cs | 7 +++ .../Repositories/PointRepository.cs | 58 ++++++++++++++----- .../Repositories/TrackRepository.cs | 56 +++++++++++++----- 7 files changed, 155 insertions(+), 30 deletions(-) diff --git a/backend/MuiCharts.Api/Controllers/PointsController.cs b/backend/MuiCharts.Api/Controllers/PointsController.cs index 3dad5e7..1fe741a 100644 --- a/backend/MuiCharts.Api/Controllers/PointsController.cs +++ b/backend/MuiCharts.Api/Controllers/PointsController.cs @@ -61,6 +61,12 @@ public class PointsController( { Logger.LogInformation("Getting points with ids {Ids}", ids); + if (ids.Length < 1) + { + Logger.LogInformation("No point IDs provided"); + return Problem([Error.Validation()]); + } + IQueryable query = await _repository.GetPointsRangeAsync(); PointResponse[] points = [ @@ -181,6 +187,29 @@ public class PointsController( return NoContent(); } + /// + /// Imports an array of points. + /// + /// The array of points to import. + /// An representing the asynchronous operation result. + [HttpPost("Import")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesDefaultResponseType(typeof(ProblemDetails))] + public async Task ImportPointsAsync(Point[] points) + { + Logger.LogInformation("Importing points"); + + ErrorOr> importResult = await _repository.AddPointsRangeAsync(points); + + if (importResult.IsError) + return Problem(importResult.Errors); + + Logger.LogInformation("Imported {Count} points", importResult.Value.Count()); + + return Ok(importResult.Value); + } + /// /// Deletes a point with the specified ID. /// @@ -189,6 +218,7 @@ public class PointsController( [HttpDelete("{id:int}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesDefaultResponseType(typeof(ProblemDetails))] public async Task DeletePointAsync(int id) { diff --git a/backend/MuiCharts.Api/Controllers/TracksController.cs b/backend/MuiCharts.Api/Controllers/TracksController.cs index a4395e3..ebc2b8e 100644 --- a/backend/MuiCharts.Api/Controllers/TracksController.cs +++ b/backend/MuiCharts.Api/Controllers/TracksController.cs @@ -138,6 +138,29 @@ public class TracksController( return NoContent(); } + /// + /// Imports tracks. + /// + /// The requests containing the track details. + /// An representing the asynchronous operation result. + [HttpPost("Import")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesDefaultResponseType(typeof(ProblemDetails))] + public async Task ImportTracksAsync(Track[] requests) + { + Logger.LogInformation("Importing tracks"); + + ErrorOr> importResult = await _repository.AddTracksRangeAsync(requests); + + if (importResult.IsError) + return Problem(importResult.Errors); + + Logger.LogInformation("Tracks imported"); + + return Ok(importResult.Value); + } + /// /// Deletes a track with the specified first point ID and second point ID. /// diff --git a/backend/MuiCharts.Contracts/Track/UpsertTrackRequest.cs b/backend/MuiCharts.Contracts/Track/UpsertTrackRequest.cs index 86b37af..b9b5287 100644 --- a/backend/MuiCharts.Contracts/Track/UpsertTrackRequest.cs +++ b/backend/MuiCharts.Contracts/Track/UpsertTrackRequest.cs @@ -7,8 +7,8 @@ namespace MuiCharts.Contracts.Track; /// Represents a request to upsert a track. /// public record UpsertTrackRequest( - [Range(0, int.MaxValue)] int FirstId, - [Range(0, int.MaxValue)] int SecondId, + [Range(1, int.MaxValue)] int FirstId, + [Range(1, int.MaxValue)] int SecondId, [Range(1, int.MaxValue)] int Distance, Surface Surface, MaxSpeed MaxSpeed diff --git a/backend/MuiCharts.Domain/Repositories/IPointRepository.cs b/backend/MuiCharts.Domain/Repositories/IPointRepository.cs index f5e5ac0..833376a 100644 --- a/backend/MuiCharts.Domain/Repositories/IPointRepository.cs +++ b/backend/MuiCharts.Domain/Repositories/IPointRepository.cs @@ -41,4 +41,11 @@ public interface IPointRepository /// The ID of the point to delete. /// A task that represents the asynchronous operation. The task result contains a flag indicating if the point was deleted successfully or an error. Task> DeletePointAsync(int id); + + /// + /// Adds a range of points asynchronously. + /// + /// The points to add. + /// A task that represents the asynchronous operation. The task result contains the added points or an error. + Task>> AddPointsRangeAsync(IEnumerable points); } diff --git a/backend/MuiCharts.Domain/Repositories/ITrackRepository.cs b/backend/MuiCharts.Domain/Repositories/ITrackRepository.cs index a1d8f51..0a73de3 100644 --- a/backend/MuiCharts.Domain/Repositories/ITrackRepository.cs +++ b/backend/MuiCharts.Domain/Repositories/ITrackRepository.cs @@ -43,4 +43,11 @@ public interface ITrackRepository /// The second ID. /// A task that represents the asynchronous operation. The task result contains the deletion status if successful, or an error if unsuccessful. Task> DeleteTrackAsync(int firstId, int secondId); + + /// + /// Adds a range of tracks asynchronously. + /// + /// The tracks to add. + /// A task that represents the asynchronous operation. The task result contains the added tracks if successful, or an error if unsuccessful. + Task>> AddTracksRangeAsync(IEnumerable tracks); } diff --git a/backend/MuiCharts.Infrastructure/Repositories/PointRepository.cs b/backend/MuiCharts.Infrastructure/Repositories/PointRepository.cs index c526e38..888f16a 100644 --- a/backend/MuiCharts.Infrastructure/Repositories/PointRepository.cs +++ b/backend/MuiCharts.Infrastructure/Repositories/PointRepository.cs @@ -26,23 +26,13 @@ public class PointRepository( { _logger.LogInformation("Adding or updating point {point}", point); - bool doesExist = _context.Points.Any(p => p.Id == point.Id); + Point result = UpsertPoint(point, out bool isNewlyCreated); + await _context.SaveChangesAsync(); - if (doesExist) - { - _logger.LogInformation("Point {id} exists, updating", point.Id); - _context.Points.Update(point); - await _context.SaveChangesAsync(); + if (!isNewlyCreated) return (Point?)null; - } - else - { - _logger.LogInformation("Point {id} does not exist, adding", point.Id); - EntityEntry result = _context.Points.Add(point); - await _context.SaveChangesAsync(); - return result.Entity; - } + return result; } catch (Exception e) { @@ -69,6 +59,29 @@ public class PointRepository( } } + public async Task>> AddPointsRangeAsync(IEnumerable points) + { + try + { + _logger.LogInformation("Adding points rage"); + + List updatedPoints = []; + + foreach (Point point in points) + updatedPoints.Add(UpsertPoint(point, out _)); + + await _context.SaveChangesAsync(); + _logger.LogInformation("Added {Count} points", updatedPoints.Count); + + return updatedPoints; + } + catch (Exception e) + { + _logger.LogError(e, "Error adding points {points}", points); + return Error.Failure(); + } + } + /// public async Task> DeletePointAsync(int id) { @@ -81,6 +94,9 @@ public class PointRepository( if (point == null) return Error.NotFound(); + if (_context.Tracks.Any(t => t.FirstId == id || t.SecondId == id)) + return Error.Conflict(description: "Point is used in a track. Delete track first"); + _context.Points.Remove(point); await _context.SaveChangesAsync(); @@ -119,4 +135,18 @@ public class PointRepository( { return Task.FromResult(_context.Points.AsQueryable()); } + + private Point UpsertPoint(Point point, out bool isNewlyCreated) + { + bool doesExist = _context.Points.Any(p => p.Id == point.Id); + isNewlyCreated = !doesExist; + Point entity; + + if (doesExist) + entity = _context.Points.Update(point).Entity; + else + entity = _context.Points.Add(point).Entity; + + return entity; + } } diff --git a/backend/MuiCharts.Infrastructure/Repositories/TrackRepository.cs b/backend/MuiCharts.Infrastructure/Repositories/TrackRepository.cs index f5671bd..5c0401b 100644 --- a/backend/MuiCharts.Infrastructure/Repositories/TrackRepository.cs +++ b/backend/MuiCharts.Infrastructure/Repositories/TrackRepository.cs @@ -32,23 +32,13 @@ public class TrackRepository( return Error.Validation(description: "One or both specified points do not exist."); } - bool doesExist = _context.Tracks.Any(t => t.FirstId == track.FirstId && t.SecondId == track.SecondId); + Track entity = UpsertTrack(track, out bool isNewlyCreated); + await _context.SaveChangesAsync(); - if (doesExist) - { - _logger.LogInformation("Track with first ID {FirstId} and second ID {SecondId} exists, updating", track.FirstId, track.SecondId); - _context.Tracks.Update(track); - await _context.SaveChangesAsync(); + if (!isNewlyCreated) return (Track?)null; - } - else - { - _logger.LogInformation("Track with first ID {FirstId} and second ID {SecondId} does not exist, adding", track.FirstId, track.SecondId); - EntityEntry result = _context.Tracks.Add(track); - await _context.SaveChangesAsync(); - return result.Entity; - } + return entity; } catch (Exception e) { @@ -88,6 +78,29 @@ public class TrackRepository( } } + public async Task>> AddTracksRangeAsync(IEnumerable tracks) + { + try + { + _logger.LogInformation("Adding tracks range"); + + List updatedTracks = []; + + foreach (Track track in tracks) + updatedTracks.Add(UpsertTrack(track, out _)); + + await _context.SaveChangesAsync(); + _logger.LogInformation("Added {Count} tracks", updatedTracks.Count); + + return updatedTracks; + } + catch (Exception e) + { + _logger.LogError(e, "Error adding tracks {tracks}", tracks); + return Error.Failure(); + } + } + /// public async Task> DeleteTrackAsync(int firstId, int secondId) { @@ -145,6 +158,21 @@ public class TrackRepository( return Task.FromResult(_context.Tracks.AsQueryable()); } + private Track UpsertTrack(Track track, out bool isNewlyCreated) + { + bool doesExist = _context.Tracks.Any(t => t.FirstId == track.FirstId && t.SecondId == track.SecondId); + isNewlyCreated = !doesExist; + + Track entity; + + if (doesExist) + entity = _context.Tracks.Update(track).Entity; + else + entity = _context.Tracks.Add(track).Entity; + + return entity; + } + private bool IsValidTrack(Track track) { return _context.Points.Any(p => p.Id == track.FirstId) &&