-
Notifications
You must be signed in to change notification settings - Fork 0
Add dashboard cruds and widgets #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
d974fe2
c2d3ca1
846f6d8
6d7d1cc
05460d0
4e1fed8
229abbc
35773e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| "banktrackingfrontend", | ||
| "bankuser", | ||
| "codegen", | ||
| "Gridster", | ||
| "healthcheck", | ||
| "isready", | ||
| "xaxis", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| using Microsoft.EntityFrameworkCore; | ||
| using PhantomDave.BankTracking.Data.UnitOfWork; | ||
| using PhantomDave.BankTracking.Library.Models; | ||
|
|
||
| namespace PhantomDave.BankTracking.Api.Services; | ||
|
|
||
| public class DashboardService | ||
| { | ||
| private readonly IUnitOfWork _unitOfWork; | ||
|
|
||
| public DashboardService(IUnitOfWork unitOfWork) | ||
| { | ||
| _unitOfWork = unitOfWork; | ||
| } | ||
|
|
||
| public Task<Dashboard?> GetDashboardAsync(int id) => | ||
| _unitOfWork.Dashboards.GetByIdAsync(id); | ||
|
|
||
| public async Task<IEnumerable<Dashboard>> GetAllDashboardsAsync() | ||
| { | ||
| var dashboards = await _unitOfWork.Dashboards | ||
| .Query() | ||
| .AsNoTracking() | ||
| .OrderBy(d => d.Id) | ||
| .ToListAsync(); | ||
|
|
||
| foreach (var dashboard in dashboards) | ||
| { | ||
| var widgets = await _unitOfWork.DashboardWidgets | ||
| .Query() | ||
| .Where(w => w.DashboardId == dashboard.Id) | ||
| .OrderBy(w => w.Id) | ||
| .ToListAsync(); | ||
| dashboard.Widgets = widgets.ToList(); | ||
| } | ||
|
|
||
| return dashboards; | ||
| } | ||
|
|
||
| public async Task<Dashboard?> CreateDashboardAsync(string name) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(name)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var normalizedName = NormalizeName(name); | ||
| if (normalizedName.Length == 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var dashboard = new Dashboard | ||
| { | ||
| Name = normalizedName, | ||
| Widgets = [] | ||
| }; | ||
|
|
||
| await _unitOfWork.Dashboards.AddAsync(dashboard); | ||
| await _unitOfWork.SaveChangesAsync(); | ||
|
|
||
| return dashboard; | ||
| } | ||
|
|
||
| public async Task<Dashboard?> UpdateDashboardAsync( | ||
| int id, | ||
| string? name = null) | ||
| { | ||
| var dashboard = await _unitOfWork.Dashboards.GetByIdAsync(id); | ||
| if (dashboard is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(name)) | ||
| { | ||
| var normalizedName = NormalizeName(name); | ||
| if (normalizedName.Length == 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| dashboard.Name = normalizedName; | ||
| } | ||
|
|
||
| await _unitOfWork.Dashboards.UpdateAsync(dashboard); | ||
| await _unitOfWork.SaveChangesAsync(); | ||
|
|
||
| return dashboard; | ||
| } | ||
|
|
||
| public async Task<bool> DeleteDashboardAsync(int id) | ||
| { | ||
| var deleted = await _unitOfWork.Dashboards.DeleteAsync(id); | ||
| if (!deleted) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| await _unitOfWork.SaveChangesAsync(); | ||
| return true; | ||
| } | ||
|
|
||
| public async Task<DashboardWidget?> AddWidgetAsync( | ||
| int dashboardId, | ||
| WidgetType type, | ||
| int x, | ||
| int y, | ||
| int rows, | ||
| int cols) | ||
| { | ||
| var dashboard = await _unitOfWork.Dashboards.GetByIdAsync(dashboardId); | ||
| if (dashboard is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| if (rows <= 0 || cols <= 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var widget = new DashboardWidget | ||
| { | ||
| DashboardId = dashboardId, | ||
| Type = type, | ||
| X = Math.Max(0, x), | ||
| Y = Math.Max(0, y), | ||
| Rows = rows, | ||
| Cols = cols | ||
| }; | ||
|
|
||
| await _unitOfWork.DashboardWidgets.AddAsync(widget); | ||
| await _unitOfWork.SaveChangesAsync(); | ||
|
|
||
| return widget; | ||
| } | ||
|
|
||
| public async Task<DashboardWidget?> UpdateWidgetAsync( | ||
| int id, | ||
| WidgetType? type = null, | ||
| int? x = null, | ||
| int? y = null, | ||
| int? rows = null, | ||
| int? cols = null) | ||
| { | ||
| var widget = await _unitOfWork.DashboardWidgets.GetByIdAsync(id); | ||
| if (widget is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| if (type.HasValue) | ||
| { | ||
| widget.Type = type.Value; | ||
| } | ||
|
|
||
| if (x.HasValue) | ||
| { | ||
| widget.X = Math.Max(0, x.Value); | ||
| } | ||
|
|
||
| if (y.HasValue) | ||
| { | ||
| widget.Y = Math.Max(0, y.Value); | ||
| } | ||
|
|
||
| if (rows.HasValue) | ||
| { | ||
| if (rows.Value <= 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| widget.Rows = rows.Value; | ||
| } | ||
|
|
||
| if (cols.HasValue) | ||
| { | ||
| if (cols.Value <= 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| widget.Cols = cols.Value; | ||
| } | ||
|
|
||
| await _unitOfWork.DashboardWidgets.UpdateAsync(widget); | ||
| await _unitOfWork.SaveChangesAsync(); | ||
|
|
||
| return widget; | ||
| } | ||
|
|
||
| public async Task<bool> RemoveWidgetAsync(int id) | ||
| { | ||
| var deleted = await _unitOfWork.DashboardWidgets.DeleteAsync(id); | ||
| if (!deleted) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| await _unitOfWork.SaveChangesAsync(); | ||
| return true; | ||
| } | ||
|
|
||
| private static string NormalizeName(string name) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(name)) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| var trimmed = name.Trim(); | ||
| return trimmed.Length <= 100 ? trimmed : trimmed[..100]; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| using PhantomDave.BankTracking.Library.Models; | ||
|
|
||
| namespace PhantomDave.BankTracking.Api.Types.Inputs; | ||
|
|
||
| public sealed class AddWidgetInput | ||
| { | ||
| public int DashboardId { get; init; } | ||
| public WidgetType Type { get; init; } | ||
| public int X { get; init; } | ||
| public int Y { get; init; } | ||
| public int Rows { get; init; } | ||
| public int Cols { get; init; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| namespace PhantomDave.BankTracking.Api.Types.Inputs; | ||
|
|
||
| public sealed class CreateDashboardInput | ||
| { | ||
| public string? Name { get; init; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace PhantomDave.BankTracking.Api.Types.Inputs; | ||
|
|
||
| public sealed class UpdateDashboardInput | ||
| { | ||
| public int Id { get; init; } | ||
| public string? Name { get; init; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| using PhantomDave.BankTracking.Library.Models; | ||
|
|
||
| namespace PhantomDave.BankTracking.Api.Types.Inputs; | ||
|
|
||
| public sealed class UpdateWidgetInput | ||
| { | ||
| public int Id { get; init; } | ||
| public WidgetType? Type { get; init; } | ||
| public int? X { get; init; } | ||
| public int? Y { get; init; } | ||
| public int? Rows { get; init; } | ||
| public int? Cols { get; init; } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||||||||||||||||||||||||
| using HotChocolate; | ||||||||||||||||||||||||||||||||||||||
| using HotChocolate.Types; | ||||||||||||||||||||||||||||||||||||||
| using PhantomDave.BankTracking.Api.Types.Inputs; | ||||||||||||||||||||||||||||||||||||||
| using PhantomDave.BankTracking.Api.Types.ObjectTypes; | ||||||||||||||||||||||||||||||||||||||
| using PhantomDave.BankTracking.Data.UnitOfWork; | ||||||||||||||||||||||||||||||||||||||
| using PhantomDave.BankTracking.Library.Models; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| namespace PhantomDave.BankTracking.Api.Types.Mutations; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| [ExtendObjectType(OperationTypeNames.Mutation)] | ||||||||||||||||||||||||||||||||||||||
| public class DashboardMutations | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| public async Task<DashboardType> CreateDashboard( | ||||||||||||||||||||||||||||||||||||||
| [Service] IUnitOfWork unitOfWork, | ||||||||||||||||||||||||||||||||||||||
| [Service] IHttpContextAccessor httpContextAccessor, | ||||||||||||||||||||||||||||||||||||||
| CreateDashboardInput input) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| var accountId = httpContextAccessor.GetAccountIdFromContext(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| var dashboard = new Dashboard | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| AccountId = accountId, | ||||||||||||||||||||||||||||||||||||||
| Name = input.Name?.Trim() ?? string.Empty | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| var dashboard = new Dashboard | |
| { | |
| AccountId = accountId, | |
| Name = input.Name?.Trim() ?? string.Empty | |
| var trimmedName = input.Name?.Trim() ?? string.Empty; | |
| if (string.IsNullOrEmpty(trimmedName)) | |
| { | |
| throw new GraphQLException( | |
| ErrorBuilder.New() | |
| .SetMessage("Dashboard name cannot be empty.") | |
| .SetCode("BAD_USER_INPUT") | |
| .Build()); | |
| } | |
| var dashboard = new Dashboard | |
| { | |
| AccountId = accountId, | |
| Name = trimmedName |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing name truncation: The CreateDashboard mutation doesn't enforce the 100-character name length limit that's defined in the database configuration (ConfigureDashboard in BankTrackerDbContext.cs line 115) and enforced in DashboardService.NormalizeName. Consider truncating the name to 100 characters before saving to prevent potential database errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
check also the security warning from codeQL
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing [Authorize] attribute: This mutation allows creating dashboards without authentication. Add the [Authorize] attribute to ensure only authenticated users can create dashboards, consistent with the query patterns in DashboardQueries.cs.
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing name truncation: The UpdateDashboard mutation doesn't truncate the name to 100 characters like DashboardService.NormalizeName does. Consider truncating the name before saving to maintain consistency with the service layer and prevent potential database errors.
| dashboard.Name = input.Name.Trim(); | |
| dashboard.Name = input.Name.Trim().Length > 100 | |
| ? input.Name.Trim().Substring(0, 100) | |
| : input.Name.Trim(); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing UpdateAsync call: The mutation modifies the dashboard entity but only calls SaveChangesAsync() without calling unitOfWork.Dashboards.UpdateAsync(dashboard). While EF Core change tracking may handle this, it's inconsistent with the pattern used in DashboardService.UpdateDashboardAsync (line 86) and other mutations. Consider calling UpdateAsync for consistency and explicit intent.
| await unitOfWork.Dashboards.UpdateAsync(dashboard); |
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing [Authorize] attribute: This mutation allows updating dashboards without authentication. Add the [Authorize] attribute to ensure only authenticated users can update dashboards, consistent with the query patterns in DashboardQueries.cs.
Copilot
AI
Nov 24, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing [Authorize] attribute: This mutation allows deleting dashboards without authentication. Add the [Authorize] attribute to ensure only authenticated users can delete dashboards, consistent with the query patterns in DashboardQueries.cs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
N+1 query problem: This method fetches dashboards in one query, then loops through each dashboard to fetch widgets individually. This results in 1 + N database queries. Consider using
.Include(d => d.Widgets)to eager-load widgets in a single query for better performance.