feat(api): workflows CRUD + activate/deactivate flag
This commit is contained in:
8
src/VolcanicCore.Api/Contracts/WorkflowDtos.cs
Normal file
8
src/VolcanicCore.Api/Contracts/WorkflowDtos.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace VolcanicCore.Api.Contracts;
|
||||
|
||||
public sealed record CreateWorkflowRequest(string Name, JsonElement Graph);
|
||||
public sealed record UpdateWorkflowRequest(string Name, JsonElement Graph);
|
||||
public sealed record WorkflowResponse(string Id, string Name, bool Active,
|
||||
JsonElement Graph, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
|
||||
49
src/VolcanicCore.Api/Endpoints/WorkflowsEndpoints.cs
Normal file
49
src/VolcanicCore.Api/Endpoints/WorkflowsEndpoints.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Text.Json;
|
||||
using VolcanicCore.Api.Contracts;
|
||||
using VolcanicCore.Persistence.Repositories;
|
||||
|
||||
namespace VolcanicCore.Api.Endpoints;
|
||||
|
||||
public static class WorkflowsEndpoints
|
||||
{
|
||||
public static void MapWorkflowsEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var g = app.MapGroup("/api/workflows").RequireAuthorization();
|
||||
|
||||
g.MapGet("", (WorkflowRepository repo) =>
|
||||
Results.Ok(repo.List().Select(ToDto)));
|
||||
|
||||
g.MapGet("/{id}", (string id, WorkflowRepository repo) =>
|
||||
{
|
||||
var w = repo.Get(id);
|
||||
return w is null ? Results.NotFound() : Results.Ok(ToDto(w));
|
||||
});
|
||||
|
||||
g.MapPost("", (CreateWorkflowRequest req, WorkflowRepository repo) =>
|
||||
{
|
||||
var id = repo.Create(req.Name, req.Graph.GetRawText());
|
||||
var w = repo.Get(id)!;
|
||||
return Results.Created($"/api/workflows/{id}", ToDto(w));
|
||||
});
|
||||
|
||||
g.MapPut("/{id}", (string id, UpdateWorkflowRequest req, WorkflowRepository repo) =>
|
||||
repo.Update(id, req.Name, req.Graph.GetRawText())
|
||||
? Results.NoContent() : Results.NotFound());
|
||||
|
||||
g.MapPost("/{id}/activate", (string id, WorkflowRepository repo) =>
|
||||
repo.SetActive(id, true) ? Results.NoContent() : Results.NotFound());
|
||||
|
||||
g.MapPost("/{id}/deactivate", (string id, WorkflowRepository repo) =>
|
||||
repo.SetActive(id, false) ? Results.NoContent() : Results.NotFound());
|
||||
|
||||
g.MapDelete("/{id}", (string id, WorkflowRepository repo) =>
|
||||
repo.Delete(id) ? Results.NoContent() : Results.NotFound());
|
||||
}
|
||||
|
||||
private static WorkflowResponse ToDto(WorkflowRow w)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(w.GraphJson);
|
||||
return new WorkflowResponse(w.Id, w.Name, w.Active, doc.RootElement.Clone(),
|
||||
w.CreatedAt, w.UpdatedAt);
|
||||
}
|
||||
}
|
||||
@@ -40,9 +40,11 @@ builder.Services.AddSingleton<Migrator>(_ => new Migrator(new IMigration[]
|
||||
{
|
||||
new Migration001Initial(),
|
||||
new Migration002Connections(),
|
||||
new Migration003Workflows(),
|
||||
}));
|
||||
builder.Services.AddSingleton<PasswordService>();
|
||||
builder.Services.AddSingleton<ConnectionRepository>();
|
||||
builder.Services.AddSingleton<WorkflowRepository>();
|
||||
builder.Services.AddSingleton<IConnectionSecretPolicy, HardcodedSecretPolicy>();
|
||||
|
||||
builder.Services
|
||||
@@ -101,6 +103,7 @@ app.MapSessionEndpoints();
|
||||
app.MapNodeTypesEndpoints();
|
||||
app.MapConnectionProvidersEndpoints();
|
||||
app.MapConnectionsEndpoints();
|
||||
app.MapWorkflowsEndpoints();
|
||||
|
||||
app.MapGet("/api/health", () => Results.Ok(new { ok = true }));
|
||||
app.Run();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence.Migrations;
|
||||
|
||||
public sealed class Migration003Workflows : IMigration
|
||||
{
|
||||
public int Version => 3;
|
||||
public void Apply(SqliteConnection conn)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE workflows (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
graph_json TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace VolcanicCore.Persistence.Repositories;
|
||||
|
||||
public sealed record WorkflowRow(string Id, string Name, string GraphJson, bool Active,
|
||||
DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
|
||||
|
||||
public sealed class WorkflowRepository
|
||||
{
|
||||
private readonly SqliteConnectionFactory _factory;
|
||||
public WorkflowRepository(SqliteConnectionFactory f) => _factory = f;
|
||||
|
||||
public string Create(string name, string graphJson)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
var now = DateTimeOffset.UtcNow.ToString("o");
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
INSERT INTO workflows (id, name, graph_json, active, created_at, updated_at)
|
||||
VALUES ($id, $name, $graph, 0, $now, $now);
|
||||
";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.Parameters.AddWithValue("$name", name);
|
||||
cmd.Parameters.AddWithValue("$graph", graphJson);
|
||||
cmd.Parameters.AddWithValue("$now", now);
|
||||
cmd.ExecuteNonQuery();
|
||||
return id;
|
||||
}
|
||||
|
||||
public WorkflowRow? Get(string id)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"SELECT id, name, graph_json, active, created_at, updated_at
|
||||
FROM workflows WHERE id = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
using var r = cmd.ExecuteReader();
|
||||
return r.Read() ? Materialize(r) : null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<WorkflowRow> List()
|
||||
{
|
||||
var rows = new List<WorkflowRow>();
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"SELECT id, name, graph_json, active, created_at, updated_at
|
||||
FROM workflows ORDER BY created_at DESC;";
|
||||
using var r = cmd.ExecuteReader();
|
||||
while (r.Read()) rows.Add(Materialize(r));
|
||||
return rows;
|
||||
}
|
||||
|
||||
public bool Update(string id, string name, string graphJson)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"UPDATE workflows SET name = $name, graph_json = $graph,
|
||||
updated_at = $now WHERE id = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.Parameters.AddWithValue("$name", name);
|
||||
cmd.Parameters.AddWithValue("$graph", graphJson);
|
||||
cmd.Parameters.AddWithValue("$now", DateTimeOffset.UtcNow.ToString("o"));
|
||||
return cmd.ExecuteNonQuery() == 1;
|
||||
}
|
||||
|
||||
public bool SetActive(string id, bool active)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"UPDATE workflows SET active = $a, updated_at = $now WHERE id = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
cmd.Parameters.AddWithValue("$a", active ? 1 : 0);
|
||||
cmd.Parameters.AddWithValue("$now", DateTimeOffset.UtcNow.ToString("o"));
|
||||
return cmd.ExecuteNonQuery() == 1;
|
||||
}
|
||||
|
||||
public bool Delete(string id)
|
||||
{
|
||||
using var conn = _factory.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"DELETE FROM workflows WHERE id = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", id);
|
||||
return cmd.ExecuteNonQuery() == 1;
|
||||
}
|
||||
|
||||
private static WorkflowRow Materialize(SqliteDataReader r) => new(
|
||||
r.GetString(0), r.GetString(1), r.GetString(2), r.GetInt64(3) != 0,
|
||||
DateTimeOffset.Parse(r.GetString(4)), DateTimeOffset.Parse(r.GetString(5)));
|
||||
}
|
||||
Reference in New Issue
Block a user