Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditLogCommands.cs
T
Joseph Doherty b2cab59cb9 feat(cli): compact table projection for audit-config query (register row 57)
The command passed no `tableProjector`, so `--format table` handed the renderer
an `AuditLogQueryResult` OBJECT. That takes the generic Property/Value branch,
which printed the entire `entries` array as raw JSON inside a single cell. JSON
output was and remains correct, and is the primary format for this command.

Adds `AuditConfigTableProjection`, mirroring `TemplateTableProjection`:
string->string over the raw body, applied only on the table path, returning the
input verbatim when it is not JSON or not a recognised shape so the renderer's
own fallbacks still apply. It unwraps the envelope to a row array — id,
timestamp, user, action, entityType, entityName, entityId — and drops
`afterStateJson`, the full post-change entity snapshot, which is the audit
analogue of template's fat child arrays: one per row fills a terminal.
`bundleImportId` is dropped as mostly-null noise.

`totalCount` would be lost by a plain unwrap, so a truncated page gains a single
trailing row with a `note` cell ("showing 50 of 137 — use --page / --page-size
for the rest"). When the page IS the whole result set the column is not emitted
at all, so a small query's table stays exactly as narrow as its data, and the
note row carries no `id` so it cannot be mistaken for an entry.

A `--detail` flag skips the projection and renders the raw payload, matching
`template list` / `template get` — that plus `--format json` is how to reach
`afterStateJson`. A bare array root is also accepted, so the projection does not
regress if the payload is ever unwrapped upstream or the deprecated `audit-log`
alias meets an older server.

Tests build their fixtures by serialising the REAL AuditLogQueryResult /
AuditLogEntry types, so renaming a server property fails the test instead of
silently blanking a column. CLI README + Component-CLI.md document the table
shape, the omitted fields, and --detail.
2026-08-15 13:22:41 -04:00

113 lines
5.9 KiB
C#

using System.CommandLine;
using System.CommandLine.Parsing;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
/// <summary>
/// The <c>scadabridge audit-config</c> command group: views the configuration-change
/// audit log (the <c>IAuditService</c> trail of admin edits — distinct from the
/// centralized append-only Audit Log served by <see cref="AuditCommands"/>).
/// </summary>
/// <remarks>
/// Renamed from <c>audit-log</c> to avoid confusion with the new
/// <c>scadabridge audit</c> group. The old <c>audit-log</c> name is retained as a
/// deprecated alias; <see cref="DeprecatedAlias"/> still resolves the full subcommand
/// tree, and <c>Program.cs</c> prints a deprecation warning when it is used.
/// </remarks>
public static class AuditLogCommands
{
/// <summary>The deprecated alias kept for backward compatibility with the old command name.</summary>
public const string DeprecatedAlias = "audit-log";
/// <summary>The deprecation warning emitted when the old <c>audit-log</c> name is used.</summary>
public const string DeprecationWarning =
"Warning: 'audit-log' is deprecated and will be removed in a future release. "
+ "Use 'audit-config' instead.";
/// <summary>
/// Writes the <see cref="DeprecationWarning"/> to <paramref name="stderr"/> when the
/// CLI was invoked via the deprecated <c>audit-log</c> command name (i.e. the first
/// argument is <see cref="DeprecatedAlias"/>). The command itself still works — it is
/// an alias of <c>audit-config</c> — so this only adds the migration warning.
/// Factored out of <c>Program.cs</c> so it is unit-testable without spawning a process.
/// </summary>
/// <param name="args">The raw command-line arguments passed to the CLI.</param>
/// <param name="stderr">The text writer to emit the deprecation warning to.</param>
public static void WriteDeprecationWarningIfNeeded(string[] args, TextWriter stderr)
{
if (args.Length > 0
&& string.Equals(args[0], DeprecatedAlias, StringComparison.Ordinal))
{
stderr.WriteLine(DeprecationWarning);
}
}
/// <summary>
/// Builds the <c>audit-config</c> command (with the deprecated <c>audit-log</c> alias) and its subcommands.
/// </summary>
/// <param name="urlOption">Global management URL option.</param>
/// <param name="formatOption">Global output format option.</param>
/// <param name="usernameOption">Global username option.</param>
/// <param name="passwordOption">Global password option.</param>
/// <returns>The configured <c>audit-config</c> command with all sub-commands registered.</returns>
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("audit-config") { Description = "Query the configuration-change audit log" };
// Backward-compatible alias for the old `audit-log` name. The alias keeps
// full subcommand parity automatically; the deprecation warning is emitted by
// the args[0] check in Program.cs.
command.Aliases.Add(DeprecatedAlias);
command.Add(BuildQuery(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildQuery(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var userOption = new Option<string?>("--user") { Description = "Filter by username" };
var entityTypeOption = new Option<string?>("--entity-type") { Description = "Filter by entity type" };
var actionOption = new Option<string?>("--action") { Description = "Filter by action" };
var fromOption = new Option<DateTimeOffset?>("--from") { Description = "Start date (ISO 8601)" };
var toOption = new Option<DateTimeOffset?>("--to") { Description = "End date (ISO 8601)" };
var pageOption = new Option<int>("--page") { Description = "Page number" };
pageOption.DefaultValueFactory = _ => 1;
var pageSizeOption = new Option<int>("--page-size") { Description = "Page size" };
pageSizeOption.DefaultValueFactory = _ => 50;
var detailOption = new Option<bool>("--detail")
{
Description = "Render the raw query payload in table output instead of the compact column projection. "
+ "This is how to see 'afterStateJson' (the full post-change entity snapshot) in table form — it is "
+ "omitted from the compact columns because a single entry's snapshot fills the terminal. "
+ "No effect on JSON output."
};
var cmd = new Command("query") { Description = "Query audit log entries" };
cmd.Add(userOption);
cmd.Add(entityTypeOption);
cmd.Add(actionOption);
cmd.Add(fromOption);
cmd.Add(toOption);
cmd.Add(pageOption);
cmd.Add(pageSizeOption);
cmd.Add(detailOption);
cmd.SetAction(async (ParseResult result) =>
{
var user = result.GetValue(userOption);
var entityType = result.GetValue(entityTypeOption);
var action = result.GetValue(actionOption);
var from = result.GetValue(fromOption);
var to = result.GetValue(toOption);
var page = result.GetValue(pageOption);
var pageSize = result.GetValue(pageSizeOption);
var detail = result.GetValue(detailOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new QueryAuditLogCommand(user, entityType, action, from, to, page, pageSize),
tableProjector: detail ? null : AuditConfigTableProjection.ProjectEntries);
});
return cmd;
}
}