-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CLI commands for managing webhook endpoints (#1754)
- Loading branch information
Showing
17 changed files
with
3,756 additions
and
5 deletions.
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
TeachingRecordSystem/src/TeachingRecordSystem.Api/GlobalUsings.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
global using AutoMapper; | ||
global using TeachingRecordSystem.Core.ApiSchema; | ||
global using PostgresModels = TeachingRecordSystem.Core.DataStore.Postgres.Models; |
333 changes: 333 additions & 0 deletions
333
TeachingRecordSystem/src/TeachingRecordSystem.Cli/Commands.WebhookEndpoint.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,333 @@ | ||
using System.CommandLine.Invocation; | ||
using System.CommandLine.Parsing; | ||
using System.Linq.Expressions; | ||
using System.Text.Json; | ||
using TeachingRecordSystem.Core.ApiSchema; | ||
using TeachingRecordSystem.Core.DataStore.Postgres; | ||
using TeachingRecordSystem.Core.DataStore.Postgres.Models; | ||
|
||
namespace TeachingRecordSystem.Cli; | ||
|
||
public partial class Commands | ||
{ | ||
public static Command CreateWebhookEndpointCommand(IConfiguration configuration) | ||
{ | ||
var jsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true }; | ||
|
||
Expression<Func<WebhookEndpoint, object>> getEndpointOutputForDisplay = e => | ||
new | ||
{ | ||
e.WebhookEndpointId, | ||
e.ApplicationUserId, | ||
ApplicationUserName = e.ApplicationUser.Name, | ||
e.ApiVersion, | ||
e.Address, | ||
e.CloudEventTypes, | ||
e.Enabled | ||
}; | ||
|
||
var command = new Command("webhook-endpoint", "Commands for managing webhook endpoints."); | ||
command.AddCommand(CreateCreateCommand()); | ||
command.AddCommand(CreateDeleteCommand()); | ||
command.AddCommand(CreateGetCommand()); | ||
command.AddCommand(CreateListCommand()); | ||
command.AddCommand(CreateUpdateCommand()); | ||
return command; | ||
|
||
static string ParseApiVersionArgument(ArgumentResult result) | ||
{ | ||
var apiVersion = NormalizeApiVersion(result.Tokens.SingleOrDefault()?.Value ?? ""); | ||
if (!VersionRegistry.AllV3MinorVersions.Contains(apiVersion)) | ||
{ | ||
result.ErrorMessage = $"'{apiVersion}' is not a valid API version."; | ||
} | ||
return apiVersion; | ||
} | ||
|
||
static string NormalizeApiVersion(string version) => version.StartsWith("V") ? version[1..] : version; | ||
|
||
Command CreateCreateCommand() | ||
{ | ||
var userIdOption = new Option<Guid>("--user-id") { IsRequired = true }; | ||
var addressOption = new Option<string>("--address") { IsRequired = true }; | ||
var cloudEventTypesOption = new Option<string[]>("--cloud-event-types") { IsRequired = true, AllowMultipleArgumentsPerToken = true }; | ||
var apiVersionOption = new Option<string>("--api-version", ParseApiVersionArgument) { IsRequired = true }; | ||
var enabledOption = new Option<bool>("--enabled") { IsRequired = false }; | ||
var connectionStringOption = new Option<string>("--connection-string") { IsRequired = true }; | ||
|
||
var configuredConnectionString = configuration.GetConnectionString("DefaultConnection"); | ||
if (configuredConnectionString is not null) | ||
{ | ||
connectionStringOption.SetDefaultValue(configuredConnectionString); | ||
} | ||
|
||
enabledOption.SetDefaultValue(true); | ||
|
||
var command = new Command("create", "Creates a webhook endpoint.") | ||
{ | ||
userIdOption, | ||
addressOption, | ||
cloudEventTypesOption, | ||
apiVersionOption, | ||
enabledOption, | ||
connectionStringOption | ||
}; | ||
|
||
command.SetHandler( | ||
async (Guid userId, string address, string[] cloudEventTypes, string apiVersion, bool enable, string connectionString) => | ||
{ | ||
await using var dbContext = TrsDbContext.Create(connectionString); | ||
|
||
var webhookEndpointId = Guid.NewGuid(); | ||
var now = DateTime.UtcNow; | ||
|
||
var endpoint = new WebhookEndpoint() | ||
{ | ||
WebhookEndpointId = webhookEndpointId, | ||
ApplicationUserId = userId, | ||
Address = address, | ||
ApiVersion = apiVersion, | ||
CloudEventTypes = cloudEventTypes.Order().ToList(), | ||
Enabled = enable, | ||
CreatedOn = now, | ||
UpdatedOn = now | ||
}; | ||
|
||
dbContext.WebhookEndpoints.Add(endpoint); | ||
|
||
dbContext.AddEventWithoutBroadcast(new WebhookEndpointCreatedEvent | ||
{ | ||
WebhookEndpoint = EventModels.WebhookEndpoint.FromModel(endpoint), | ||
EventId = Guid.NewGuid(), | ||
CreatedUtc = now, | ||
RaisedBy = SystemUser.SystemUserId | ||
}); | ||
|
||
await dbContext.SaveChangesAsync(); | ||
|
||
var printableEndpoint = await dbContext.WebhookEndpoints | ||
.Where(e => e.WebhookEndpointId == webhookEndpointId) | ||
.OrderBy(e => e.CreatedOn) | ||
.Select(getEndpointOutputForDisplay) | ||
.SingleAsync(); | ||
|
||
var output = JsonSerializer.Serialize(printableEndpoint, jsonSerializerOptions); | ||
Console.WriteLine(output); | ||
}, | ||
userIdOption, | ||
addressOption, | ||
cloudEventTypesOption, | ||
apiVersionOption, | ||
enabledOption, | ||
connectionStringOption); | ||
|
||
return command; | ||
} | ||
|
||
Command CreateDeleteCommand() | ||
{ | ||
var webhookEndpointIdOption = new Option<Guid>(["--webhook-endpoint-id", "--id"]) { IsRequired = true }; | ||
var connectionStringOption = new Option<string>("--connection-string") { IsRequired = true }; | ||
|
||
var configuredConnectionString = configuration.GetConnectionString("DefaultConnection"); | ||
if (configuredConnectionString is not null) | ||
{ | ||
connectionStringOption.SetDefaultValue(configuredConnectionString); | ||
} | ||
|
||
var command = new Command("delete", "Deletes a webhook endpoint.") | ||
{ | ||
webhookEndpointIdOption, | ||
connectionStringOption | ||
}; | ||
|
||
command.SetHandler( | ||
async (Guid webhookEndpointId, string connectionString) => | ||
{ | ||
await using var dbContext = TrsDbContext.Create(connectionString); | ||
|
||
var endpoint = await dbContext.WebhookEndpoints | ||
.SingleAsync(e => e.WebhookEndpointId == webhookEndpointId); | ||
|
||
var now = DateTime.UtcNow; | ||
endpoint.DeletedOn = now; | ||
|
||
dbContext.AddEventWithoutBroadcast(new WebhookEndpointDeletedEvent | ||
{ | ||
WebhookEndpoint = EventModels.WebhookEndpoint.FromModel(endpoint), | ||
EventId = Guid.NewGuid(), | ||
CreatedUtc = now, | ||
RaisedBy = SystemUser.SystemUserId | ||
}); | ||
|
||
await dbContext.SaveChangesAsync(); | ||
}, | ||
webhookEndpointIdOption, | ||
connectionStringOption); | ||
|
||
return command; | ||
} | ||
|
||
Command CreateGetCommand() | ||
{ | ||
var webhookEndpointIdOption = new Option<Guid>(["--webhook-endpoint-id", "--id"]) { IsRequired = true }; | ||
var connectionStringOption = new Option<string>("--connection-string") { IsRequired = true }; | ||
|
||
var configuredConnectionString = configuration.GetConnectionString("DefaultConnection"); | ||
if (configuredConnectionString is not null) | ||
{ | ||
connectionStringOption.SetDefaultValue(configuredConnectionString); | ||
} | ||
|
||
var command = new Command("get", "Gets a webhook endpoint.") | ||
{ | ||
webhookEndpointIdOption, | ||
connectionStringOption | ||
}; | ||
|
||
command.SetHandler( | ||
async (Guid webhookEndpointId, string connectionString) => | ||
{ | ||
await using var dbContext = TrsDbContext.Create(connectionString); | ||
|
||
var endpoint = await dbContext.WebhookEndpoints | ||
.Include(e => e.ApplicationUser) | ||
.SingleAsync(e => e.WebhookEndpointId == webhookEndpointId); | ||
|
||
var printableEndpoint = new[] { endpoint }.AsQueryable().Select(getEndpointOutputForDisplay).Single(); | ||
|
||
var output = JsonSerializer.Serialize(printableEndpoint, jsonSerializerOptions); | ||
Console.WriteLine(output); | ||
}, | ||
webhookEndpointIdOption, | ||
connectionStringOption); | ||
|
||
return command; | ||
} | ||
|
||
Command CreateListCommand() | ||
{ | ||
var connectionStringOption = new Option<string>("--connection-string") { IsRequired = true }; | ||
|
||
var configuredConnectionString = configuration.GetConnectionString("DefaultConnection"); | ||
if (configuredConnectionString is not null) | ||
{ | ||
connectionStringOption.SetDefaultValue(configuredConnectionString); | ||
} | ||
|
||
var command = new Command("list", "Lists the webhook endpoints.") | ||
{ | ||
connectionStringOption | ||
}; | ||
|
||
command.SetHandler( | ||
async (string connectionString) => | ||
{ | ||
await using var dbContext = TrsDbContext.Create(connectionString); | ||
|
||
var endpoints = await dbContext.WebhookEndpoints | ||
.Where(e => e.ApplicationUser.Active) | ||
.OrderBy(e => e.CreatedOn) | ||
.Select(getEndpointOutputForDisplay) | ||
.ToListAsync(); | ||
|
||
var output = JsonSerializer.Serialize(endpoints, jsonSerializerOptions); | ||
Console.WriteLine(output); | ||
}, | ||
connectionStringOption); | ||
|
||
return command; | ||
} | ||
|
||
Command CreateUpdateCommand() | ||
{ | ||
var webhookEndpointIdOption = new Option<Guid>(["--webhook-endpoint-id", "--id"]) { IsRequired = true }; | ||
var addressOption = new Option<string>("--address") { IsRequired = false }; | ||
var cloudEventTypesOption = new Option<string[]>("--cloud-event-types") { IsRequired = false, AllowMultipleArgumentsPerToken = true }; | ||
var apiVersionOption = new Option<string>("--api-version", ParseApiVersionArgument) { IsRequired = false }; | ||
var enabledOption = new Option<bool>("--enabled") { IsRequired = false }; | ||
var connectionStringOption = new Option<string>("--connection-string") { IsRequired = true }; | ||
|
||
var configuredConnectionString = configuration.GetConnectionString("DefaultConnection"); | ||
if (configuredConnectionString is not null) | ||
{ | ||
connectionStringOption.SetDefaultValue(configuredConnectionString); | ||
} | ||
|
||
enabledOption.SetDefaultValue(true); | ||
|
||
var command = new Command("update", "Updates a webhook endpoint.") | ||
{ | ||
webhookEndpointIdOption, | ||
addressOption, | ||
cloudEventTypesOption, | ||
apiVersionOption, | ||
enabledOption, | ||
connectionStringOption | ||
}; | ||
|
||
command.SetHandler( | ||
async (InvocationContext context) => | ||
{ | ||
var webhookEndpointId = context.ParseResult.GetValueForOption(webhookEndpointIdOption); | ||
var connectionString = context.ParseResult.GetValueForOption(connectionStringOption)!; | ||
|
||
await using var dbContext = TrsDbContext.Create(connectionString); | ||
|
||
var endpoint = await dbContext.WebhookEndpoints | ||
.Include(e => e.ApplicationUser) | ||
.SingleAsync(e => e.WebhookEndpointId == webhookEndpointId); | ||
|
||
var changes = WebhookEndpointUpdatedChanges.None; | ||
|
||
if (context.ParseResult.HasOption(addressOption)) | ||
{ | ||
endpoint.Address = context.ParseResult.GetValueForOption(addressOption)!; | ||
changes |= WebhookEndpointUpdatedChanges.Address; | ||
} | ||
|
||
if (context.ParseResult.HasOption(cloudEventTypesOption)) | ||
{ | ||
endpoint.CloudEventTypes = context.ParseResult.GetValueForOption(cloudEventTypesOption)!.Order().ToList(); | ||
changes |= WebhookEndpointUpdatedChanges.CloudEventTypes; | ||
} | ||
|
||
if (context.ParseResult.HasOption(apiVersionOption)) | ||
{ | ||
endpoint.ApiVersion = context.ParseResult.GetValueForOption(apiVersionOption)!; | ||
changes |= WebhookEndpointUpdatedChanges.ApiVersion; | ||
} | ||
|
||
if (context.ParseResult.HasOption(enabledOption)) | ||
{ | ||
endpoint.Enabled = context.ParseResult.GetValueForOption(enabledOption); | ||
changes |= WebhookEndpointUpdatedChanges.Enabled; | ||
} | ||
|
||
if (changes != WebhookEndpointUpdatedChanges.None) | ||
{ | ||
var now = DateTime.UtcNow; | ||
endpoint.UpdatedOn = now; | ||
|
||
dbContext.AddEventWithoutBroadcast(new WebhookEndpointUpdatedEvent | ||
{ | ||
EventId = Guid.NewGuid(), | ||
CreatedUtc = now, | ||
RaisedBy = SystemUser.SystemUserId, | ||
WebhookEndpoint = EventModels.WebhookEndpoint.FromModel(endpoint), | ||
Changes = changes | ||
}); | ||
|
||
await dbContext.SaveChangesAsync(); | ||
} | ||
|
||
var printableEndpoint = new[] { endpoint }.AsQueryable().Select(getEndpointOutputForDisplay).Single(); | ||
|
||
var output = JsonSerializer.Serialize(printableEndpoint, jsonSerializerOptions); | ||
Console.WriteLine(output); | ||
}); | ||
|
||
return command; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 3 additions & 1 deletion
4
...achingRecordSystem.Api/VersionRegistry.cs → ...dSystem.Core/ApiSchema/VersionRegistry.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.