b2cab59cb9
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.
154 lines
5.9 KiB
C#
154 lines
5.9 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
|
|
/// <summary>
|
|
/// Compact table projection for <c>audit-config query</c> (deferred-work register row 57).
|
|
///
|
|
/// <para>
|
|
/// The handler returns an <c>AuditLogQueryResult</c> — <c>{ "entries": [ … ],
|
|
/// "totalCount": N }</c>. Handed to the generic renderer unprojected, that object takes
|
|
/// the Property/Value branch and the whole <c>entries</c> array lands in a single cell as
|
|
/// raw JSON, so <c>--format table</c> was unreadable. This unwraps the envelope to the
|
|
/// row array the renderer expects and drops <c>afterStateJson</c>, the per-entry snapshot
|
|
/// of the entire changed entity — the same "one fat field makes the table useless" problem
|
|
/// <see cref="TemplateTableProjection"/> solves by dropping child collections.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <c>totalCount</c> would be lost by a plain unwrap, so when the page is a strict subset
|
|
/// of the matches a single trailing row carries a <c>note</c> cell saying how many of how
|
|
/// many are shown. Every real entry leaves that cell empty, and when the page IS the whole
|
|
/// result set the column is not emitted at all — so the common small-query table stays
|
|
/// exactly as narrow as its data.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// JSON output is never touched (this runs only on the table path), and the command's
|
|
/// <c>--detail</c> flag skips the projection to render the raw payload as-is — that is
|
|
/// the way to see <c>afterStateJson</c> in table form.
|
|
/// </para>
|
|
/// </summary>
|
|
internal static class AuditConfigTableProjection
|
|
{
|
|
/// <summary>
|
|
/// Projects an <c>AuditLogQueryResult</c> envelope (or a bare entry array) to the
|
|
/// compact row array the table renderer consumes. Returns the input unchanged when it
|
|
/// is not JSON or not one of those shapes, so the renderer's own fallbacks still apply.
|
|
/// </summary>
|
|
/// <param name="json">The raw success JSON body from the management API.</param>
|
|
/// <returns>Compact JSON suitable for table rendering.</returns>
|
|
internal static string ProjectEntries(string json)
|
|
{
|
|
JsonDocument doc;
|
|
try
|
|
{
|
|
doc = JsonDocument.Parse(json);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Not JSON (e.g. a proxy error page) — let the renderer print it verbatim.
|
|
return json;
|
|
}
|
|
|
|
using (doc)
|
|
{
|
|
var root = doc.RootElement;
|
|
|
|
// A bare array is accepted so the projection still works if the payload is
|
|
// ever unwrapped upstream, and so the deprecated `audit-log` alias against an
|
|
// older server does not render worse than it does today.
|
|
if (root.ValueKind == JsonValueKind.Array)
|
|
{
|
|
return BuildRows(root, totalCount: null).ToJsonString();
|
|
}
|
|
|
|
if (root.ValueKind == JsonValueKind.Object
|
|
&& TryGetPropertyCI(root, "entries", out var entries)
|
|
&& entries.ValueKind == JsonValueKind.Array)
|
|
{
|
|
int? total = TryGetPropertyCI(root, "totalCount", out var tc)
|
|
&& tc.ValueKind == JsonValueKind.Number
|
|
&& tc.TryGetInt32(out var n)
|
|
? n
|
|
: null;
|
|
return BuildRows(entries, total).ToJsonString();
|
|
}
|
|
|
|
// Some other object (e.g. an error envelope) — leave it to the renderer.
|
|
return json;
|
|
}
|
|
}
|
|
|
|
private static JsonArray BuildRows(JsonElement entries, int? totalCount)
|
|
{
|
|
var arr = new JsonArray();
|
|
var shown = 0;
|
|
foreach (var item in entries.EnumerateArray())
|
|
{
|
|
arr.Add(ProjectElement(item));
|
|
shown++;
|
|
}
|
|
|
|
// Only when the page is a strict subset — otherwise the count is just the row
|
|
// count the operator can already see, and the extra column would be noise.
|
|
if (totalCount is int total && total > shown)
|
|
{
|
|
arr.Add(new JsonObject
|
|
{
|
|
["note"] = JsonValue.Create(
|
|
$"showing {shown} of {total} — use --page / --page-size for the rest"),
|
|
});
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
|
|
/// <summary>Projects a single audit entry to its compact row node.</summary>
|
|
private static JsonNode ProjectElement(JsonElement element)
|
|
{
|
|
if (element.ValueKind != JsonValueKind.Object)
|
|
return JsonValue.Create(element.ToString())!;
|
|
|
|
// JsonObject preserves insertion order, which fixes the table's column order.
|
|
// afterStateJson (the full post-change entity snapshot) and bundleImportId are
|
|
// deliberately omitted — `--detail`, or --format json, for those.
|
|
return new JsonObject
|
|
{
|
|
["id"] = Int(element, "id"),
|
|
["timestamp"] = Str(element, "timestamp"),
|
|
["user"] = Str(element, "user"),
|
|
["action"] = Str(element, "action"),
|
|
["entityType"] = Str(element, "entityType"),
|
|
["entityName"] = Str(element, "entityName"),
|
|
["entityId"] = Str(element, "entityId"),
|
|
};
|
|
}
|
|
|
|
private static bool TryGetPropertyCI(JsonElement obj, string name, out JsonElement value)
|
|
{
|
|
foreach (var prop in obj.EnumerateObject())
|
|
{
|
|
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
value = prop.Value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
private static JsonNode? Str(JsonElement obj, string name)
|
|
=> TryGetPropertyCI(obj, name, out var v) && v.ValueKind == JsonValueKind.String
|
|
? JsonValue.Create(v.GetString())
|
|
: null;
|
|
|
|
private static JsonNode? Int(JsonElement obj, string name)
|
|
=> TryGetPropertyCI(obj, name, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var n)
|
|
? JsonValue.Create(n)
|
|
: null;
|
|
}
|