feat(management): secured-write list paging with TotalCount (arch-review P3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <c>secured-write</c> command group. Currently exposes the read-only
|
||||
/// <c>list</c> sub-command, which relays a <see cref="ListSecuredWritesCommand"/> to the
|
||||
/// central <c>ManagementActor</c> (gated any-of <c>Operator</c> / <c>Verifier</c> /
|
||||
/// <c>Administrator</c>). The list is offset-paged via <c>--skip</c> / <c>--take</c>
|
||||
/// (arch-review P3); omitting them preserves the historical unpaged default (first 200 rows).
|
||||
/// </summary>
|
||||
public static class SecuredWriteCommands
|
||||
{
|
||||
// Options are static so the parsed values can be read back from both the SetAction
|
||||
// and the internal BuildListCommand helper (used by command-construction tests).
|
||||
private static readonly Option<string?> StatusOption =
|
||||
new("--status") { Description = "Filter by status (e.g. Pending, Approved, Rejected, Expired, Executed, Failed); omit to match all" };
|
||||
private static readonly Option<string?> SiteOption =
|
||||
new("--site") { Description = "Filter by site identifier; omit to match all sites" };
|
||||
private static readonly Option<int> SkipOption =
|
||||
new("--skip") { Description = "Number of rows to skip (offset paging)", DefaultValueFactory = _ => 0 };
|
||||
private static readonly Option<int> TakeOption =
|
||||
new("--take") { Description = "Maximum rows to return (clamped to 1..500 server-side)", DefaultValueFactory = _ => 200 };
|
||||
|
||||
/// <summary>Builds the <c>secured-write</c> command group with its <c>list</c> sub-command.</summary>
|
||||
/// <param name="urlOption">Global <c>--url</c> option for the management API endpoint.</param>
|
||||
/// <param name="formatOption">Global <c>--format</c> option for output format.</param>
|
||||
/// <param name="usernameOption">Global <c>--username</c> option for authentication.</param>
|
||||
/// <param name="passwordOption">Global <c>--password</c> option for authentication.</param>
|
||||
/// <returns>The configured <c>secured-write</c> command.</returns>
|
||||
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
||||
{
|
||||
var command = new Command("secured-write") { Description = "Query two-person (MxGateway) secured writes" };
|
||||
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
|
||||
return command;
|
||||
}
|
||||
|
||||
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
||||
{
|
||||
var cmd = new Command("list") { Description = "List secured writes (pending queue + terminal history), optionally filtered and paged" };
|
||||
cmd.Add(StatusOption);
|
||||
cmd.Add(SiteOption);
|
||||
cmd.Add(SkipOption);
|
||||
cmd.Add(TakeOption);
|
||||
cmd.SetAction(async (ParseResult result) =>
|
||||
{
|
||||
return await CommandHelpers.ExecuteCommandAsync(
|
||||
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||
BuildListCommand(result));
|
||||
});
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="ListSecuredWritesCommand"/> from a parsed <c>secured-write list</c>
|
||||
/// invocation. Exposed internally so command-construction tests can assert field mapping
|
||||
/// (including the <c>--skip</c> / <c>--take</c> paging options) without triggering the HTTP call.
|
||||
/// </summary>
|
||||
/// <param name="result">The parsed command-line result from the <c>secured-write list</c> invocation.</param>
|
||||
/// <returns>A <see cref="ListSecuredWritesCommand"/> populated from the parsed result.</returns>
|
||||
internal static ListSecuredWritesCommand BuildListCommand(ParseResult result)
|
||||
{
|
||||
var status = result.GetValue(StatusOption);
|
||||
var site = result.GetValue(SiteOption);
|
||||
var skip = result.GetValue(SkipOption);
|
||||
var take = result.GetValue(TakeOption);
|
||||
return new ListSecuredWritesCommand(status, site, skip, take);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ rootCommand.Add(DbConnectionCommands.Build(urlOption, formatOption, usernameOpti
|
||||
rootCommand.Add(ApiMethodCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
|
||||
rootCommand.Add(BundleCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
|
||||
rootCommand.Add(CachedCallCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
|
||||
rootCommand.Add(SecuredWriteCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
|
||||
|
||||
rootCommand.SetAction(_ =>
|
||||
{
|
||||
|
||||
@@ -51,12 +51,16 @@ public record ApproveSecuredWriteCommand(long Id, string? Comment);
|
||||
public record RejectSecuredWriteCommand(long Id, string? Comment);
|
||||
|
||||
/// <summary>
|
||||
/// Read-only query for secured writes, optionally filtered by status and site.
|
||||
/// A <c>null</c> filter matches every row.
|
||||
/// Read-only query for secured writes, optionally filtered by status and site,
|
||||
/// with offset paging. A <c>null</c> filter matches every row. The <paramref name="Skip"/>
|
||||
/// / <paramref name="Take"/> defaults preserve the historical unpaged wire behaviour
|
||||
/// (first 200 rows) for callers that omit them (additive evolution).
|
||||
/// </summary>
|
||||
/// <param name="Status">Status filter; <c>null</c> matches every status.</param>
|
||||
/// <param name="SiteId">Site id filter; <c>null</c> matches every site.</param>
|
||||
public record ListSecuredWritesCommand(string? Status, string? SiteId);
|
||||
/// <param name="Skip">Number of rows to skip (offset paging); clamped to >= 0 server-side.</param>
|
||||
/// <param name="Take">Maximum rows to return; clamped to 1..500 server-side. Default 200.</param>
|
||||
public record ListSecuredWritesCommand(string? Status, string? SiteId, int Skip = 0, int Take = 200);
|
||||
|
||||
/// <summary>
|
||||
/// Result projection of a single pending secured write row, mirroring the
|
||||
@@ -79,5 +83,11 @@ public record SecuredWriteDto(
|
||||
DateTime? ExecutedAtUtc,
|
||||
string? ExecutionError);
|
||||
|
||||
/// <summary>Result wrapper for <see cref="ListSecuredWritesCommand"/>.</summary>
|
||||
public record SecuredWriteListResult(IReadOnlyList<SecuredWriteDto> Items);
|
||||
/// <summary>
|
||||
/// Result wrapper for <see cref="ListSecuredWritesCommand"/>. <paramref name="TotalCount"/>
|
||||
/// is the count of rows matching the query filters ignoring paging, so the UI can render
|
||||
/// a pager independent of the returned page size.
|
||||
/// </summary>
|
||||
/// <param name="Items">The page of matching secured-write rows.</param>
|
||||
/// <param name="TotalCount">Total number of rows matching the filters (unpaged).</param>
|
||||
public record SecuredWriteListResult(IReadOnlyList<SecuredWriteDto> Items, int TotalCount);
|
||||
|
||||
@@ -1394,7 +1394,13 @@ public class ManagementActor : ReceiveActor
|
||||
IServiceProvider sp, ListSecuredWritesCommand cmd)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISecuredWriteRepository>();
|
||||
var rows = await repo.QueryAsync(cmd.Status, cmd.SiteId, skip: 0, take: 200);
|
||||
|
||||
// Offset paging (arch-review P3): clamp untrusted paging inputs so a crafted
|
||||
// command can neither request a negative offset nor an unbounded page.
|
||||
var take = Math.Clamp(cmd.Take, 1, 500);
|
||||
var skip = Math.Max(0, cmd.Skip);
|
||||
var rows = await repo.QueryAsync(cmd.Status, cmd.SiteId, skip, take);
|
||||
var totalCount = await repo.CountAsync(cmd.Status, cmd.SiteId);
|
||||
|
||||
// Opportunistic expiry sweep (arch-review S2): every time the list is read, walk
|
||||
// the page and expire any overdue Pending row. TryExpireIfStaleAsync self-filters
|
||||
@@ -1411,7 +1417,7 @@ public class ManagementActor : ReceiveActor
|
||||
await TryExpireIfStaleAsync(sp, repo, row);
|
||||
}
|
||||
|
||||
return new SecuredWriteListResult(rows.Select(ToSecuredWriteDto).ToList());
|
||||
return new SecuredWriteListResult(rows.Select(ToSecuredWriteDto).ToList(), totalCount);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user