Complete management API and CLI command parity

This commit is contained in:
Joseph Doherty
2026-09-10 00:51:45 -04:00
parent 477e2abe97
commit 2750bd2809
38 changed files with 1637 additions and 199 deletions
@@ -23,6 +23,8 @@ public static class NotificationCommands
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildRename(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildRecipient(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSmtp(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSms(urlOption, formatOption, usernameOption, passwordOption));
@@ -30,6 +32,49 @@ public static class NotificationCommands
return command;
}
private static Option<int> RequiredId(string name, string description)
{
var option = new Option<int>(name) { Description = description, Required = true };
option.Validators.Add(result => { if (result.GetValueOrDefault<int>() <= 0) result.AddError($"{name} must be positive."); });
return option;
}
private static Command BuildRename(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("rename") { Description = "Rename a list, preserving all recipients and their IDs" };
var id = RequiredId("--id", "Notification list ID");
var name = new Option<string>("--name") { Required = true, Description = "New list name" };
cmd.Add(id); cmd.Add(name);
cmd.SetAction(async result => await CommandHelpers.ExecuteCommandAsync(result, urlOption, formatOption, usernameOption, passwordOption,
new RenameNotificationListCommand(result.GetValue(id), result.GetValue(name)!)));
return cmd;
}
private static Command BuildRecipient(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("recipient") { Description = "Manage individual named recipients without replacing the list" };
var add = new Command("add") { Description = "Add a named recipient (address must match the list delivery channel)" };
var listId = RequiredId("--list-id", "Notification list ID");
var name = new Option<string>("--name") { Required = true, Description = "Recipient name" };
var email = new Option<string?>("--email") { Description = "Email address" };
var phone = new Option<string?>("--phone") { Description = "Phone number in E.164 format" };
add.Add(listId); add.Add(name); add.Add(email); add.Add(phone);
add.Validators.Add(result =>
{
if (string.IsNullOrWhiteSpace(result.GetValue(email)) && string.IsNullOrWhiteSpace(result.GetValue(phone)))
result.AddError("Provide --email and/or --phone for the recipient.");
});
add.SetAction(async result => await CommandHelpers.ExecuteCommandAsync(result, urlOption, formatOption, usernameOption, passwordOption,
new AddNotificationRecipientCommand(result.GetValue(listId), result.GetValue(name)!, result.GetValue(email), result.GetValue(phone))));
var delete = new Command("delete") { Description = "Delete one recipient by its recipient ID" };
var id = RequiredId("--id", "Recipient ID");
delete.Add(id);
delete.SetAction(async result => await CommandHelpers.ExecuteCommandAsync(result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteNotificationRecipientCommand(result.GetValue(id))));
group.Add(add); group.Add(delete);
return group;
}
// ------------------------------------------------------------------------
// Notification list create/update options (static so the parsed values can
// be read back both from the SetAction and from the testable Build* helpers).
@@ -134,6 +179,15 @@ public static class NotificationCommands
});
group.Add(listCmd);
var createCmd = new Command("create") { Description = "Create SMTP configuration" };
foreach (var option in new Option[] { SmtpServerOption, SmtpPortOption, SmtpAuthModeOption, SmtpFromOption, SmtpTlsModeOption,
SmtpCredentialsOption, SmtpOAuth2AuthorityOption, SmtpOAuth2ScopeOption, SmtpTransportOption }) createCmd.Add(option);
createCmd.SetAction(async result => await CommandHelpers.ExecuteCommandAsync(result, urlOption, formatOption, usernameOption, passwordOption,
new CreateSmtpConfigCommand(result.GetValue(SmtpServerOption)!, result.GetValue(SmtpPortOption), result.GetValue(SmtpAuthModeOption)!,
result.GetValue(SmtpFromOption)!, result.GetValue(SmtpTlsModeOption), result.GetValue(SmtpCredentialsOption),
result.GetValue(SmtpOAuth2AuthorityOption), result.GetValue(SmtpOAuth2ScopeOption), result.GetValue(SmtpTransportOption))));
group.Add(createCmd);
var updateCmd = new Command("update") { Description = "Update SMTP configuration" };
updateCmd.Add(SmtpIdOption);
updateCmd.Add(SmtpServerOption);
@@ -247,6 +301,21 @@ public static class NotificationCommands
});
group.Add(listCmd);
var createCmd = new Command("create") { Description = "Create SMS configuration (Auth Token required)" };
foreach (var option in new Option[] { SmsAccountSidOption, SmsFromNumberOption, SmsMessagingServiceSidOption, SmsApiBaseUrlOption,
SmsAuthTokenOption, SmsTimeoutOption, SmsMaxRetriesOption, SmsRetryDelayOption }) createCmd.Add(option);
createCmd.Validators.Add(result =>
{
if (string.IsNullOrWhiteSpace(result.GetValue(SmsAuthTokenOption))) result.AddError("--auth-token is required when creating SMS configuration.");
if (string.IsNullOrWhiteSpace(result.GetValue(SmsFromNumberOption)) && string.IsNullOrWhiteSpace(result.GetValue(SmsMessagingServiceSidOption)))
result.AddError("Provide --from-number and/or --messaging-service-sid.");
});
createCmd.SetAction(async result => await CommandHelpers.ExecuteCommandAsync(result, urlOption, formatOption, usernameOption, passwordOption,
new CreateSmsConfigCommand(result.GetValue(SmsAccountSidOption)!, result.GetValue(SmsFromNumberOption), result.GetValue(SmsMessagingServiceSidOption),
result.GetValue(SmsApiBaseUrlOption), result.GetValue(SmsAuthTokenOption), result.GetValue(SmsTimeoutOption),
result.GetValue(SmsMaxRetriesOption), SmsRetryDelay(result))));
group.Add(createCmd);
var updateCmd = new Command("update") { Description = "Update SMS configuration" };
updateCmd.Add(SmsIdOption);
updateCmd.Add(SmsAccountSidOption);
@@ -254,6 +323,9 @@ public static class NotificationCommands
updateCmd.Add(SmsMessagingServiceSidOption);
updateCmd.Add(SmsApiBaseUrlOption);
updateCmd.Add(SmsAuthTokenOption);
updateCmd.Add(SmsTimeoutOption);
updateCmd.Add(SmsMaxRetriesOption);
updateCmd.Add(SmsRetryDelayOption);
updateCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
@@ -265,6 +337,24 @@ public static class NotificationCommands
return group;
}
private static readonly Option<int?> SmsTimeoutOption = SmsPolicyOption("--connection-timeout-seconds", "Connection timeout in seconds", 1);
private static readonly Option<int?> SmsMaxRetriesOption = SmsPolicyOption("--max-retries", "Retry count (zero uses the outbox fallback)", 0);
private static readonly Option<int?> SmsRetryDelayOption = SmsPolicyOption("--retry-delay-seconds", "Retry delay in seconds (zero uses the outbox fallback)", 0);
private static Option<int?> SmsPolicyOption(string name, string description, int minimum)
{
var option = new Option<int?>(name) { Description = description + "; omission preserves updates or uses create defaults" };
option.Validators.Add(result =>
{
if (result.GetValueOrDefault<int?>() is { } value && value < minimum)
result.AddError($"{name} must be at least {minimum}.");
});
return option;
}
private static TimeSpan? SmsRetryDelay(ParseResult result) =>
result.GetValue(SmsRetryDelayOption) is { } seconds ? TimeSpan.FromSeconds(seconds) : null;
// SMS update options are static so the parsed values can be read back both
// from the SetAction and from BuildUpdateSmsConfigCommand (used by tests).
private static readonly Option<int> SmsIdOption =
@@ -317,7 +407,8 @@ public static class NotificationCommands
throw new ArgumentException(
"Provide --from-number and/or --messaging-service-sid (at least one sender identity is required).");
}
return new UpdateSmsConfigCommand(id, accountSid, fromNumber, messagingServiceSid, apiBaseUrl, authToken);
return new UpdateSmsConfigCommand(id, accountSid, fromNumber, messagingServiceSid, apiBaseUrl, authToken,
result.GetValue(SmsTimeoutOption), result.GetValue(SmsMaxRetriesOption), SmsRetryDelay(result));
}
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)