ConfigurePermisssions

Add Get Endpoints
Add entity framework Core  feature SqlLite implementation
This commit is contained in:
Jérémie DEVILLARD 2025-02-12 23:56:10 +01:00
parent 0aed8208dc
commit 421ace1f5b
24 changed files with 485 additions and 89 deletions

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elsa.Connections.Api;
public class Constants
{
public const string PermissionsNamespace = "connections";
}

View file

@ -2,19 +2,19 @@
using Elsa.Connections.Contracts;
using Elsa.Workflows.Models;
namespace Elsa.Connections.Api.Endpoints.ActivityConnectionDescriptor.List;
namespace Elsa.Connections.Api.Endpoints.ActivityConnectionDescriptor.Get;
public class Get(IConnectionDescriptorRegistry store) : ElsaEndpointWithoutRequest<IEnumerable<InputDescriptor>>
public class Endpoint(IConnectionDescriptorRegistry store) : ElsaEndpointWithoutRequest<IEnumerable<InputDescriptor>>
{
public override void Configure()
{
Get("/connection-configuration/input-descriptor/{ActivityType}");
AllowAnonymous();
ConfigurePermissions($"{Constants.PermissionsNamespace}/descriptor:read");
}
public override async Task HandleAsync(CancellationToken ct)
{
string type = Route<string>("ActivityType");
var type = Route<string>("ActivityType");
var config = await store.GetConnectionDescriptor(type);
if (config == null)

View file

@ -22,7 +22,7 @@ public class List(IConnectionDescriptorRegistry registry) : ElsaEndpointWithoutR
public override void Configure()
{
Get("/connection-configuration/descriptors");
AllowAnonymous();
ConfigurePermissions($"{Constants.PermissionsNamespace}/descriptor:read");
}
public override Task<PagedListResponse<ConnectionDescriptor>> ExecuteAsync(CancellationToken ct)

View file

@ -14,7 +14,7 @@ public class Endpoint(IConnectionStore store, IIdentityGenerator identityGenerat
public override void Configure()
{
Post("/connection-configuration");
AllowAnonymous();
ConfigurePermissions($"{Constants.PermissionsNamespace}:write");
}
public override async Task<ConnectionModel> ExecuteAsync(ConnectionInputModel model, CancellationToken ct)
@ -52,53 +52,3 @@ public class Endpoint(IConnectionStore store, IIdentityGenerator identityGenerat
}
}
public class EndpointUpdate(IConnectionStore store) : ElsaEndpoint<ConnectionInputModel, ConnectionModel>
{
public override void Configure()
{
Put("/connection-configuration/{id}");
AllowAnonymous();
}
public override async Task<ConnectionModel> ExecuteAsync(ConnectionInputModel model, CancellationToken ct)
{
var id = Route<string>("id")!;
var entity = await store.GetAsync(id, ct);
if(entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}
var isNameDuplicate = await IsNameDuplicateAsync(model.Name, id, ct);
if (isNameDuplicate)
{
AddError("Another connection already exist with the specified name");
await SendErrorsAsync(cancellation: ct);
return entity.ToModel();
}
entity.Name = model.Name;
entity.Description = model.Description;
entity.ConnectionConfiguration = model.ConnectionConfiguration;
await store.UpdateAsync(entity, ct);
return entity.ToModel();
}
private async Task<bool> IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken)
{
var entities = await store.FindAsync(new ConnectionDefinitionFilter
{
NotId = id,
Name = name
});
return !(entities == null);
}
}

View file

@ -9,7 +9,7 @@ public class Endpoint(IConnectionStore store) : ElsaEndpoint<Request>
public override void Configure()
{
Delete("/connection-configuration/{id}");
AllowAnonymous();
ConfigurePermissions($"{Constants.PermissionsNamespace}:delete");
}
public override async Task HandleAsync(Request req, CancellationToken ct)

View file

@ -0,0 +1,33 @@
using Elsa.Abstractions;
using Elsa.Connections.Models;
using Elsa.Connections.Persistence.Contracts;
using Elsa.Connections.Api.Extensions;
namespace Elsa.Connections.Api.Endpoints.Get;
public class Endpoint(IConnectionStore store) : ElsaEndpoint<Request,ConnectionModel>
{
public override void Configure()
{
Get("/connection-configuration/{id}");
ConfigurePermissions($"{Constants.PermissionsNamespace}:read");
}
public override async Task<ConnectionModel> ExecuteAsync(Request req, CancellationToken ct)
{
var entity = await store.FindAsync(
new Persistence.Filters.ConnectionDefinitionFilter
{
Id = req.Id
}
);
if (entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}
return entity.ToModel();
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elsa.Connections.Api.Endpoints.Get;
public class Request
{
[Required] public string Id { get; set; } = default!;
}

View file

@ -15,7 +15,7 @@ public class Endpoint(IConnectionStore store) : ElsaEndpointWithoutRequest<Paged
public override void Configure()
{
Get("/connection-configuration");
AllowAnonymous();
ConfigurePermissions($"{Constants.PermissionsNamespace}:read");
}
public override async Task<PagedListResponse<ConnectionModel>> ExecuteAsync(CancellationToken ct)

View file

@ -0,0 +1,58 @@
using Elsa.Abstractions;
using Elsa.Connections.Models;
using Elsa.Connections.Persistence.Contracts;
using Elsa.Connections.Persistence.Filters;
using Elsa.Connections.Api.Extensions;
namespace Elsa.Connections.Api.Endpoints.Update;
public class Endpoint(IConnectionStore store) : ElsaEndpoint<ConnectionInputModel, ConnectionModel>
{
public override void Configure()
{
Put("/connection-configuration/{id}");
ConfigurePermissions($"{Constants.PermissionsNamespace}:write");
}
public override async Task<ConnectionModel> ExecuteAsync(ConnectionInputModel model, CancellationToken ct)
{
var id = Route<string>("id")!;
var entity = await store.GetAsync(id, ct);
if (entity == null)
{
await SendNotFoundAsync(ct);
return null!;
}
var isNameDuplicate = await IsNameDuplicateAsync(model.Name, id, ct);
if (isNameDuplicate)
{
AddError("Another connection already exist with the specified name");
await SendErrorsAsync(cancellation: ct);
return entity.ToModel();
}
entity.Name = model.Name;
entity.Description = model.Description;
entity.ConnectionConfiguration = model.ConnectionConfiguration;
await store.UpdateAsync(entity, ct);
return entity.ToModel();
}
private async Task<bool> IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken)
{
var entities = await store.FindAsync(new ConnectionDefinitionFilter
{
NotId = id,
Name = name
});
return !(entities == null);
}
}

View file

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Elsa.Connections.Models;
public class ConnectionConfigurationModel
{
public string Id { get; set; }
[Required]
public string? Name { get; set; }
public string Description { get; set; }
public JsonObject ConnectionConfiguration { get; set; }
[Required]
public string ConnectionType { get; set; }
}

View file

@ -1,13 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Configs\" />
</ItemGroup>
<PropertyGroup>
<Description>Provides models that can be shared between the API and API clients</Description>
<PackageTags>elsa module connections api</PackageTags>
<RootNamespace>Elsa.Connections</RootNamespace>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,15 @@
using Elsa.EntityFrameworkCore.Abstractions;
using Elsa.EntityFrameworkCore.Extensions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite;
[UsedImplicitly]
public class SqliteConnectionDbContextFactory : DesignTimeDbContextFactoryBase<ConnectionDbContext>
{
protected override void ConfigureBuilder(DbContextOptionsBuilder<ConnectionDbContext> builder, string connectionString)
{
builder.UseElsaSqlite(GetType().Assembly, connectionString);
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides an EF Core migrations for SQLite for the Connection Persistence module.</Description>
<PackageTags>elsa module persistence efcore entity framework core sqlite</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Connections.Persistence.EntityFrameworkCore\Elsa.Connections.Persistence.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,60 @@
// <auto-generated />
using Elsa.Connections.Persistence.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.Migrations
{
[DbContext(typeof(ConnectionDbContext))]
[Migration("20250212220543_V3_3")]
partial class V3_3
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Elsa")
.HasAnnotation("ProductVersion", "8.0.12");
modelBuilder.Entity("Elsa.Connections.Persistence.Entities.ConnectionDefinition", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConnectionConfiguration")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ConnectionType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TenantId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_ConnectionDefinition_Name");
b.HasIndex("TenantId")
.HasDatabaseName("IX_ConnectionDefinition_TenantId");
b.ToTable("ConnectionDefinitions", "Elsa");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.Migrations
{
/// <inheritdoc />
public partial class V3_3 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_3(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
{
_schema = schema;
}
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: _schema.Schema);
migrationBuilder.CreateTable(
name: "ConnectionDefinitions",
schema: _schema.Schema,
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
ConnectionConfiguration = table.Column<string>(type: "TEXT", nullable: false),
ConnectionType = table.Column<string>(type: "TEXT", nullable: false),
TenantId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ConnectionDefinitions", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ConnectionDefinition_Name",
schema: _schema.Schema,
table: "ConnectionDefinitions",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_ConnectionDefinition_TenantId",
schema: _schema.Schema,
table: "ConnectionDefinitions",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ConnectionDefinitions",
schema: _schema.Schema);
}
}
}

View file

@ -0,0 +1,57 @@
// <auto-generated />
using Elsa.Connections.Persistence.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.Migrations
{
[DbContext(typeof(ConnectionDbContext))]
partial class ConnectionDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Elsa")
.HasAnnotation("ProductVersion", "8.0.12");
modelBuilder.Entity("Elsa.Connections.Persistence.Entities.ConnectionDefinition", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConnectionConfiguration")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ConnectionType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TenantId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.HasDatabaseName("IX_ConnectionDefinition_Name");
b.HasIndex("TenantId")
.HasDatabaseName("IX_ConnectionDefinition_TenantId");
b.ToTable("ConnectionDefinitions", "Elsa");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,28 @@
using System.Reflection;
using Elsa.Connections.Persistence.EntityFrameworkCore;
// ReSharper disable once CheckNamespace
namespace Elsa.EntityFrameworkCore.Extensions;
/// <summary>
/// Provides extensions to configure EF Core to use Sqlite.
/// </summary>
public static class ConnectionsSqliteProvidersExtensions
{
private static Assembly Assembly => typeof(ConnectionsSqliteProvidersExtensions).Assembly;
/// <summary>
/// Configures the feature to use Sqlite.
/// </summary>
public static EFCoreConnectionPersistenceFeature UseSqlite(this EFCoreConnectionPersistenceFeature feature, string? connectionString = null, ElsaDbContextOptions? options = null)
{
feature.UseSqlite(Assembly, connectionString, options);
return feature;
}
public static EFCoreConnectionPersistenceFeature UseSqlite(this EFCoreConnectionPersistenceFeature feature, Func<IServiceProvider, string> connectionStringFunc, ElsaDbContextOptions? options = null)
{
feature.UseSqlite(Assembly, connectionStringFunc, options);
return feature;
}
}

View file

@ -0,0 +1 @@
ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_3 -c ConnectionDbContext -o Migrations"

View file

@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using Elsa.Connections.Models;
using Elsa.Connections.Persistence.Entities;
using Elsa.EntityFrameworkCore.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@ -13,7 +14,9 @@ public class Configurations : IEntityTypeConfiguration<ConnectionDefinition>
{
public void Configure(EntityTypeBuilder<ConnectionDefinition> builder)
{
builder.Property(x => x.ConnectionConfiguration).HasJsonValueConversion();
builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(ConnectionDefinition)}_{nameof(ConnectionDefinition.Name)}");
builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(ConnectionDefinition)}_{nameof(ConnectionDefinition.TenantId)}");
}
}

View file

@ -0,0 +1,54 @@
using Elsa.Connections.Persistence.Contracts;
using Elsa.Connections.Persistence.Entities;
using Elsa.Connections.Persistence.Filters;
using Elsa.EntityFrameworkCore;
using JetBrains.Annotations;
namespace Elsa.Connections.Persistence.EntityFrameworkCore;
/// <summary>
/// An EF Core implementation of <see cref="IConnectionStore"/>.
/// </summary>
[UsedImplicitly]
public class EFCoreConnectionStore(EntityStore<ConnectionDbContext, ConnectionDefinition> store) : IConnectionStore
{
public Task AddAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default)
{
return store.AddAsync(entity, cancellationToken);
}
public Task UpdateAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default)
{
return store.UpdateAsync(entity, cancellationToken);
}
public Task<ConnectionDefinition> GetAsync(string id, CancellationToken cancellationToken = default)
{
var filter = new ConnectionDefinitionFilter
{
Id = id
};
return FindAsync(filter, cancellationToken);
}
public Task<ConnectionDefinition> FindAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default)
{
return store.FindAsync(filter.Apply, cancellationToken);
}
public Task<IEnumerable<ConnectionDefinition>> ListAsync(CancellationToken cancellationToken = default)
{
return store.ListAsync(cancellationToken);
}
public Task DeleteAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default)
{
return store.DeleteAsync(entity, cancellationToken);
}
public Task<IEnumerable<ConnectionDefinition>> FindManyAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default)
{
return store.QueryAsync(filter.Apply, cancellationToken);
}
}

View file

@ -0,0 +1,22 @@
using Elsa.Connections.Persistence.Features;
using Elsa.Connections.Persistence.EntityFrameworkCore;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Elsa.Agents;
/// <summary>
/// Provides extensions to the <see cref="ConnectionPersistenceFeature"/> feature.
/// </summary>
[PublicAPI]
public static class Extensions
{
/// <summary>
/// Configures the <see cref="ConnectionPersistenceFeature"/> to use EF Core persistence providers.
/// </summary>
public static ConnectionPersistenceFeature UseEntityFrameworkCore(this ConnectionPersistenceFeature feature, Action<EFCoreConnectionPersistenceFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
}

View file

@ -0,0 +1,34 @@
using Elsa.Connections.Persistence.Entities;
using Elsa.Connections.Persistence.Features;
using Elsa.EntityFrameworkCore;
using Elsa.EntityFrameworkCore.EntityHandlers;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Connections.Persistence.EntityFrameworkCore;
/// <summary>
/// Configures the default workflow runtime to use EF Core persistence providers.
/// </summary>
[DependsOn(typeof(ConnectionPersistenceFeature))]
public class EFCoreConnectionPersistenceFeature(IModule module) : PersistenceFeatureBase<EFCoreConnectionPersistenceFeature, ConnectionDbContext>(module)
{
/// <inheritdoc />
public override void Configure()
{
Module.Configure<ConnectionPersistenceFeature>(feature =>
{
feature
.UseConnectionStore(sp=> sp.GetRequiredService<EFCoreConnectionStore>())
;
});
}
/// <inheritdoc />
public override void Apply()
{
base.Apply();
AddEntityStore<ConnectionDefinition, EFCoreConnectionStore>();
}
}