Skip to content

Commit 5b43f62

Browse files
committed
Improve ngo endpoints
1 parent 90fa35c commit 5b43f62

File tree

16 files changed

+231
-462
lines changed

16 files changed

+231
-462
lines changed

api/src/Vote.Monitor.Api.Feature.Ngo/Activate/Endpoint.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Vote.Monitor.Api.Feature.Ngo.Activate;
1+
using Authorization.Policies;
2+
3+
namespace Vote.Monitor.Api.Feature.Ngo.Activate;
24

35
public class Endpoint(IRepository<NgoAggregate> repository) : Endpoint<Request, Results<NoContent, NotFound>>
46
{
@@ -7,6 +9,9 @@ public override void Configure()
79
{
810
Post("/api/ngos/{id}:activate");
911
Description(x => x.Accepts<Request>());
12+
DontAutoTag();
13+
Options(x => x.WithTags("ngos"));
14+
Policies(PolicyNames.PlatformAdminsOnly);
1015
}
1116

1217
public override async Task<Results<NoContent, NotFound>> ExecuteAsync(Request req, CancellationToken ct)

api/src/Vote.Monitor.Api.Feature.Ngo/Create/Endpoint.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
using Vote.Monitor.Api.Feature.Ngo.Specifications;
1+
using Authorization.Policies;
2+
using Vote.Monitor.Api.Feature.Ngo.Specifications;
23

34
namespace Vote.Monitor.Api.Feature.Ngo.Create;
45

56
public class Endpoint(IRepository<NgoAggregate> repository) :
6-
Endpoint<Request, Results<Ok<NgoModel>, Conflict<ProblemDetails>>>
7+
Endpoint<Request, Results<Ok<NgoModel>, Conflict<ProblemDetails>>>
78
{
8-
99
public override void Configure()
1010
{
1111
Post("/api/ngos");
12+
DontAutoTag();
13+
Options(x => x.WithTags("ngos"));
14+
Policies(PolicyNames.PlatformAdminsOnly);
1215
}
1316

14-
public override async Task<Results<Ok<NgoModel>, Conflict<ProblemDetails>>> ExecuteAsync(Request req, CancellationToken ct)
17+
public override async Task<Results<Ok<NgoModel>, Conflict<ProblemDetails>>> ExecuteAsync(Request req,
18+
CancellationToken ct)
1519
{
1620
var specification = new GetNgoByNameSpecification(req.Name);
1721
var hasNgoWithSameName = await repository.AnyAsync(specification, ct);
@@ -30,8 +34,9 @@ public override async Task<Results<Ok<NgoModel>, Conflict<ProblemDetails>>> Exec
3034
Id = ngo.Id,
3135
Name = ngo.Name,
3236
Status = ngo.Status,
33-
CreatedOn = ngo.CreatedOn,
34-
LastModifiedOn = ngo.LastModifiedOn
37+
NumberOfElectionsMonitoring = 0,
38+
NumberOfNgoAdmins = 0,
39+
DateOfLastElection = null
3540
});
3641
}
3742
}

api/src/Vote.Monitor.Api.Feature.Ngo/Deactivate/Endpoint.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
namespace Vote.Monitor.Api.Feature.Ngo.Deactivate;
1+
using Authorization.Policies;
2+
3+
namespace Vote.Monitor.Api.Feature.Ngo.Deactivate;
24

35
public class Endpoint(IRepository<NgoAggregate> repository) : Endpoint<Request, Results<NoContent, NotFound>>
46
{
57
public override void Configure()
68
{
79
Post("/api/ngos/{id}:deactivate");
810
Description(x => x.Accepts<Request>());
11+
DontAutoTag();
12+
Options(x => x.WithTags("ngos"));
13+
Policies(PolicyNames.PlatformAdminsOnly);
914
}
1015

1116
public override async Task<Results<NoContent, NotFound>> ExecuteAsync(Request req, CancellationToken ct)

api/src/Vote.Monitor.Api.Feature.Ngo/Delete/Endpoint.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
namespace Vote.Monitor.Api.Feature.Ngo.Delete;
1+
using Authorization.Policies;
2+
3+
namespace Vote.Monitor.Api.Feature.Ngo.Delete;
24

35
public class Endpoint(IRepository<NgoAggregate> repository) : Endpoint<Request, Results<NoContent, NotFound, ProblemDetails>>
46
{
57
public override void Configure()
68
{
79
Delete("/api/ngos/{id}");
10+
DontAutoTag();
11+
Options(x => x.WithTags("ngos"));
12+
Policies(PolicyNames.PlatformAdminsOnly);
813
}
914

1015
public override async Task<Results<NoContent, NotFound, ProblemDetails>> ExecuteAsync(Request req, CancellationToken ct)
Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,57 @@
1-
namespace Vote.Monitor.Api.Feature.Ngo.Get;
1+
using Authorization.Policies;
2+
using Dapper;
3+
using Vote.Monitor.Domain.ConnectionFactory;
24

3-
public class Endpoint(IReadRepository<NgoAggregate> repository) : Endpoint<Request, Results<Ok<NgoModel>, NotFound>>
5+
namespace Vote.Monitor.Api.Feature.Ngo.Get;
6+
7+
public class Endpoint(INpgsqlConnectionFactory dbConnectionFactory) : Endpoint<Request, Results<Ok<NgoModel>, NotFound>>
48
{
59
public override void Configure()
610
{
711
Get("/api/ngos/{id}");
12+
DontAutoTag();
13+
Options(x => x.WithTags("ngos"));
14+
Policies(PolicyNames.PlatformAdminsOnly);
815
}
916

10-
public override async Task<Results<Ok<NgoModel>, NotFound>> ExecuteAsync(Request req, CancellationToken ct)
17+
public override async Task<Results<Ok<NgoModel>, NotFound>> ExecuteAsync(Request req,
18+
CancellationToken ct)
1119
{
12-
var ngo = await repository.GetByIdAsync(req.Id, ct);
20+
var sql = """
21+
SELECT
22+
N."Id",
23+
N."Name",
24+
N."Status",
25+
(SELECT COUNT(1)
26+
FROM "NgoAdmins" NA
27+
WHERE NA."NgoId" = N."Id") AS "NumberOfNgoAdmins",
28+
(SELECT COUNT(1)
29+
FROM "MonitoringNgos" MN
30+
WHERE MN."NgoId" = N."Id") AS "NumberOfElectionsMonitoring",
31+
(
32+
SELECT MAX(ER."StartDate")
33+
FROM "MonitoringNgos" MN
34+
INNER JOIN "ElectionRounds" ER ON ER."Id" = MN."ElectionRoundId"
35+
WHERE MN."NgoId" = N."Id"
36+
) AS "DateOfLastElection"
37+
FROM
38+
"Ngos" N
39+
WHERE
40+
N."Id" = @ngoId
41+
""";
1342

14-
if (ngo is null)
15-
{
16-
return TypedResults.NotFound();
17-
}
43+
var queryArgs = new { ngoId = req.Id };
1844

19-
return TypedResults.Ok(new NgoModel
45+
46+
using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct))
2047
{
21-
Id = ngo.Id,
22-
Name = ngo.Name,
23-
Status = ngo.Status,
24-
CreatedOn = ngo.CreatedOn,
25-
LastModifiedOn = ngo.LastModifiedOn
26-
});
48+
var ngoModel = await dbConnection.QuerySingleOrDefaultAsync<NgoModel>(sql, queryArgs);
49+
if (ngoModel is null)
50+
{
51+
return TypedResults.NotFound();
52+
}
53+
54+
return TypedResults.Ok(ngoModel);
55+
}
2756
}
2857
}
Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,157 @@
1-
using Vote.Monitor.Api.Feature.Ngo.Specifications;
1+
using Authorization.Policies;
2+
using Dapper;
23
using Vote.Monitor.Core.Models;
4+
using Vote.Monitor.Domain.ConnectionFactory;
5+
using Vote.Monitor.Domain.Specifications;
36

47
namespace Vote.Monitor.Api.Feature.Ngo.List;
58

6-
public class Endpoint(IReadRepository<NgoAggregate> repository) : Endpoint<Request, Results<Ok<PagedResponse<NgoModel>>, ProblemDetails>>
9+
public class Endpoint(INpgsqlConnectionFactory dbConnectionFactory)
10+
: Endpoint<Request, Results<Ok<PagedResponse<NgoModel>>, NotFound>>
711
{
812
public override void Configure()
913
{
1014
Get("/api/ngos");
15+
DontAutoTag();
16+
Options(x => x.WithTags("ngos"));
17+
Policies(PolicyNames.PlatformAdminsOnly);
1118
}
1219

13-
public override async Task<Results<Ok<PagedResponse<NgoModel>>, ProblemDetails>> ExecuteAsync(Request req, CancellationToken ct)
20+
public override async Task<Results<Ok<PagedResponse<NgoModel>>, NotFound>> ExecuteAsync(Request req,
21+
CancellationToken ct)
1422
{
15-
var specification = new ListNgosSpecification(req);
16-
var ngos = await repository.ListAsync(specification, ct);
17-
var ngosCount = await repository.CountAsync(specification, ct);
23+
var sql = """
24+
SELECT
25+
COUNT(*) AS COUNT
26+
FROM
27+
"Ngos" N
28+
WHERE
29+
(
30+
@searchText IS NULL
31+
OR N."Name" ILIKE @searchText
32+
OR N."Id"::TEXT ILIKE @searchText
33+
)
34+
AND (@status IS NULL OR N."Status" = @status);
1835
19-
var result = ngos.Select(x => new NgoModel
36+
WITH CTE_Ngos AS (
37+
SELECT
38+
N."Id",
39+
N."Name",
40+
N."Status",
41+
(SELECT COUNT(1)
42+
FROM "NgoAdmins" NA
43+
WHERE NA."NgoId" = N."Id") AS "NumberOfNgoAdmins",
44+
(SELECT COUNT(1)
45+
FROM "MonitoringNgos" MN
46+
WHERE MN."NgoId" = N."Id") AS "NumberOfElectionsMonitoring",
47+
(
48+
SELECT MAX(ER."StartDate")
49+
FROM "MonitoringNgos" MN
50+
INNER JOIN "ElectionRounds" ER ON ER."Id" = MN."ElectionRoundId"
51+
WHERE MN."NgoId" = N."Id"
52+
) AS "DateOfLastElection"
53+
FROM
54+
"Ngos" N
55+
WHERE
56+
(
57+
@searchText IS NULL
58+
OR N."Name" ILIKE @searchText
59+
OR N."Id"::TEXT ILIKE @searchText
60+
)
61+
AND (@status IS NULL OR N."Status" = @status)
62+
)
63+
SELECT * FROM CTE_Ngos
64+
ORDER BY
65+
CASE
66+
WHEN @sortExpression = 'Name ASC' THEN "Name"
67+
END ASC,
68+
CASE
69+
WHEN @sortExpression = 'Name DESC' THEN "Name"
70+
END DESC,
71+
CASE
72+
WHEN @sortExpression = 'Status ASC' THEN "Status"
73+
END ASC,
74+
CASE
75+
WHEN @sortExpression = 'Status DESC' THEN "Status"
76+
END DESC,
77+
CASE
78+
WHEN @sortExpression = 'NumberOfNgoAdmins ASC' THEN "NumberOfNgoAdmins"
79+
END ASC,
80+
CASE
81+
WHEN @sortExpression = 'NumberOfNgoAdmins DESC' THEN "NumberOfNgoAdmins"
82+
END DESC,
83+
CASE
84+
WHEN @sortExpression = 'NumberOfElectionsMonitoring ASC' THEN "NumberOfElectionsMonitoring"
85+
END ASC,
86+
CASE
87+
WHEN @sortExpression = 'NumberOfElectionsMonitoring DESC' THEN "NumberOfElectionsMonitoring"
88+
END DESC,
89+
CASE
90+
WHEN @sortExpression = 'DateOfLastElection ASC' THEN "DateOfLastElection"
91+
END ASC,
92+
CASE
93+
WHEN @sortExpression = 'DateOfLastElection DESC' THEN "DateOfLastElection"
94+
END DESC
95+
OFFSET @offset ROWS
96+
FETCH NEXT @pageSize ROWS ONLY;
97+
""";
98+
99+
var queryArgs = new
100+
{
101+
searchText = $"%{req.SearchText?.Trim() ?? string.Empty}%",
102+
status = req.Status?.ToString(),
103+
offset = PaginationHelper.CalculateSkip(req.PageSize, req.PageNumber),
104+
pageSize = req.PageSize,
105+
sortExpression = GetSortExpression(req.SortColumnName, req.IsAscendingSorting)
106+
};
107+
108+
int totalRowCount;
109+
List<NgoModel> entries;
110+
111+
using (var dbConnection = await dbConnectionFactory.GetOpenConnectionAsync(ct))
112+
{
113+
using var multi = await dbConnection.QueryMultipleAsync(sql, queryArgs);
114+
totalRowCount = multi.Read<int>().Single();
115+
entries = multi.Read<NgoModel>().ToList();
116+
}
117+
118+
return TypedResults.Ok(
119+
new PagedResponse<NgoModel>(entries, totalRowCount, req.PageNumber, req.PageSize));
120+
}
121+
122+
private static string GetSortExpression(string? sortColumnName, bool isAscendingSorting)
123+
{
124+
if (string.IsNullOrWhiteSpace(sortColumnName))
20125
{
21-
Id = x.Id,
22-
Name = x.Name,
23-
Status = x.Status,
24-
CreatedOn = x.CreatedOn,
25-
LastModifiedOn = x.LastModifiedOn
26-
}).ToList();
27-
28-
return TypedResults.Ok(new PagedResponse<NgoModel>(result, ngosCount, req.PageNumber, req.PageSize));
126+
return $"{nameof(NgoModel.Name)} ASC";
127+
}
128+
129+
var sortOrder = isAscendingSorting ? "ASC" : "DESC";
130+
131+
if (string.Equals(sortColumnName, nameof(NgoModel.Status),
132+
StringComparison.InvariantCultureIgnoreCase))
133+
{
134+
return $"{nameof(NgoModel.Status)} {sortOrder}";
135+
}
136+
137+
if (string.Equals(sortColumnName, nameof(NgoModel.NumberOfNgoAdmins),
138+
StringComparison.InvariantCultureIgnoreCase))
139+
{
140+
return $"{nameof(NgoModel.NumberOfNgoAdmins)} {sortOrder}";
141+
}
142+
143+
if (string.Equals(sortColumnName, nameof(NgoModel.NumberOfElectionsMonitoring),
144+
StringComparison.InvariantCultureIgnoreCase))
145+
{
146+
return $"{nameof(NgoModel.NumberOfElectionsMonitoring)} {sortOrder}";
147+
}
148+
149+
if (string.Equals(sortColumnName, nameof(NgoModel.DateOfLastElection),
150+
StringComparison.InvariantCultureIgnoreCase))
151+
{
152+
return $"{nameof(NgoModel.DateOfLastElection)} {sortOrder}";
153+
}
154+
155+
return $"{nameof(NgoModel.Name)} DESC";
29156
}
30157
}
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
using System.Text.Json.Serialization;
2-
using Ardalis.SmartEnum.SystemTextJson;
3-
using Vote.Monitor.Core.Models;
1+
using Vote.Monitor.Core.Models;
42
using Vote.Monitor.Domain.Entities.NgoAggregate;
53

64
namespace Vote.Monitor.Api.Feature.Ngo.List;
75

8-
public class Request: BaseSortPaginatedRequest
6+
public class Request : BaseSortPaginatedRequest
97
{
10-
[QueryParam]
11-
public string? SearchText { get; set; }
8+
[QueryParam] public string? SearchText { get; set; }
129

13-
[QueryParam]
14-
[JsonConverter(typeof(SmartEnumNameConverter<NgoStatus, string>))]
15-
public NgoStatus? Status { get; set; }
10+
[QueryParam] public NgoStatus? Status { get; set; }
1611
}
Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
using System.Text.Json.Serialization;
2-
using Ardalis.SmartEnum.SystemTextJson;
3-
using Vote.Monitor.Domain.Entities.NgoAggregate;
1+
using Vote.Monitor.Domain.Entities.NgoAggregate;
42

53
namespace Vote.Monitor.Api.Feature.Ngo;
64

75
public record NgoModel
86
{
97
public Guid Id { get; init; }
10-
public required string Name { get; init; }
11-
12-
[JsonConverter(typeof(SmartEnumNameConverter<NgoStatus, string>))]
13-
public required NgoStatus Status { get; init; }
14-
public required DateTime CreatedOn { get; init; }
15-
public required DateTime? LastModifiedOn { get; init; }
8+
public string Name { get; init; }
9+
public NgoStatus Status { get; init; }
10+
public int NumberOfNgoAdmins { get; init; }
11+
public int NumberOfElectionsMonitoring { get; init; }
12+
public DateOnly? DateOfLastElection { get; set; }
1613
}

api/src/Vote.Monitor.Api.Feature.Ngo/Specifications/GetNgoByNameSpecification.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,4 @@ public GetNgoByNameSpecification(string name)
77
Query
88
.Where(x => x.Name == name);
99
}
10-
11-
1210
}

0 commit comments

Comments
 (0)