From 5c0d8b0f4e2ec380e0629db3ba1456f47869b7a5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 8 Jun 2026 15:22:59 +0200 Subject: [PATCH] Move AI EF Core migrations to provider projects --- Elsa.sln | 103 +++++++-- .../DbContextFactories.cs | 24 ++ .../Elsa.AI.Persistence.EFCore.MySql.csproj | 19 ++ .../MySqlAIPersistenceFeatureExtensions.cs | 30 +++ .../FodyWeavers.xml | 4 + .../AI/20260608124957_Initial.Designer.cs | 208 +++++++++++++++++ .../Migrations/AI/20260608124957_Initial.cs | 213 ++++++++++++++++++ .../Migrations/AI/AIDbContextModelSnapshot.cs | 205 +++++++++++++++++ .../MySqlAIPersistenceShellFeature.cs | 34 +++ .../DbContextFactories.cs | 19 ++ .../Elsa.AI.Persistence.EFCore.Oracle.csproj | 17 ++ .../OracleAIPersistenceFeatureExtensions.cs | 30 +++ .../FodyWeavers.xml | 4 + .../AI/20260608125003_Initial.Designer.cs | 208 +++++++++++++++++ .../Migrations/AI/20260608125003_Initial.cs | 174 ++++++++++++++ .../Migrations/AI/AIDbContextModelSnapshot.cs | 205 +++++++++++++++++ .../OracleAIPersistenceShellFeature.cs | 28 +++ .../DbContextFactories.cs | 23 ++ ...sa.AI.Persistence.EFCore.PostgreSql.csproj | 17 ++ ...ostgreSqlAIPersistenceFeatureExtensions.cs | 30 +++ .../FodyWeavers.xml | 4 + .../AI/20260608124950_Initial.Designer.cs | 208 +++++++++++++++++ .../Migrations/AI/20260608124950_Initial.cs | 174 ++++++++++++++ .../Migrations/AI/AIDbContextModelSnapshot.cs | 205 +++++++++++++++++ .../PostgreSqlAIPersistenceShellFeature.cs | 34 +++ .../DbContextFactories.cs | 17 ++ ...lsa.AI.Persistence.EFCore.SqlServer.csproj | 17 ++ ...SqlServerAIPersistenceFeatureExtensions.cs | 30 +++ .../FodyWeavers.xml | 4 + .../AI/20260608124944_Initial.Designer.cs | 208 +++++++++++++++++ .../Migrations/AI/20260608124944_Initial.cs | 174 ++++++++++++++ .../Migrations/AI/AIDbContextModelSnapshot.cs | 205 +++++++++++++++++ .../SqlServerAIPersistenceShellFeature.cs | 27 +++ .../DbContextFactories.cs | 23 ++ .../Elsa.AI.Persistence.EFCore.Sqlite.csproj | 17 ++ .../SqliteAIPersistenceFeatureExtensions.cs | 31 +++ .../FodyWeavers.xml | 4 + .../AI/20260608124921_Initial.Designer.cs | 207 +++++++++++++++++ .../Migrations/AI/20260608124921_Initial.cs | 174 ++++++++++++++ .../Migrations/AI/AIDbContextModelSnapshot.cs | 204 +++++++++++++++++ .../SqliteAIPersistenceShellFeature.cs | 34 +++ .../Elsa.AI.Persistence.EFCore/AIDbContext.cs | 16 +- .../Elsa.AI.Persistence.EFCore.csproj | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 6 + .../Features/AIPersistenceFeature.cs | 12 +- .../20260521002000_Initial.Designer.cs | 158 ------------- .../Migrations/20260521002000_Initial.cs | 138 ------------ .../Migrations/AIDbContextModelSnapshot.cs | 156 ------------- .../ShellFeatures/AIPersistenceFeature.cs | 25 -- .../EFCoreAIPersistenceShellFeatureBase.cs | 19 ++ .../AIDbContextOptionsBuilderExtensions.cs | 15 ++ .../EFCoreAIAuditSinkTests.cs | 2 +- .../EFCoreAIConversationStoreTests.cs | 2 +- .../EFCoreAIProposalStoreTests.cs | 6 +- ...lsa.AI.Persistence.EFCore.UnitTests.csproj | 1 + 55 files changed, 3653 insertions(+), 501 deletions(-) create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/DbContextFactories.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/Elsa.AI.Persistence.EFCore.MySql.csproj create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/Extensions/MySqlAIPersistenceFeatureExtensions.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/FodyWeavers.xml create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.Designer.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/AIDbContextModelSnapshot.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.MySql/ShellFeatures/MySqlAIPersistenceShellFeature.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/DbContextFactories.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/Elsa.AI.Persistence.EFCore.Oracle.csproj create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/Extensions/OracleAIPersistenceFeatureExtensions.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/FodyWeavers.xml create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.Designer.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/AIDbContextModelSnapshot.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Oracle/ShellFeatures/OracleAIPersistenceShellFeature.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/DbContextFactories.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Elsa.AI.Persistence.EFCore.PostgreSql.csproj create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Extensions/PostgreSqlAIPersistenceFeatureExtensions.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/FodyWeavers.xml create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.Designer.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/AIDbContextModelSnapshot.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/ShellFeatures/PostgreSqlAIPersistenceShellFeature.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/DbContextFactories.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Elsa.AI.Persistence.EFCore.SqlServer.csproj create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Extensions/SqlServerAIPersistenceFeatureExtensions.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/FodyWeavers.xml create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.Designer.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/AIDbContextModelSnapshot.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.SqlServer/ShellFeatures/SqlServerAIPersistenceShellFeature.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/DbContextFactories.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Elsa.AI.Persistence.EFCore.Sqlite.csproj create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Extensions/SqliteAIPersistenceFeatureExtensions.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/FodyWeavers.xml create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.Designer.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/AIDbContextModelSnapshot.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore.Sqlite/ShellFeatures/SqliteAIPersistenceShellFeature.cs delete mode 100644 src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.Designer.cs delete mode 100644 src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.cs delete mode 100644 src/modules/Elsa.AI.Persistence.EFCore/Migrations/AIDbContextModelSnapshot.cs delete mode 100644 src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/AIPersistenceFeature.cs create mode 100644 src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/EFCoreAIPersistenceShellFeatureBase.cs create mode 100644 test/unit/Elsa.AI.Persistence.EFCore.UnitTests/AIDbContextOptionsBuilderExtensions.cs diff --git a/Elsa.sln b/Elsa.sln index beab0717a..15243aa3b 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -427,6 +427,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "keyvalues", "keyvalues", "{ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dashboard", "dashboard", "{DD8D1712-B42C-4BF2-99AF-3F092E508C62}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Persistence.EFCore.Sqlite", "src\modules\Elsa.AI.Persistence.EFCore.Sqlite\Elsa.AI.Persistence.EFCore.Sqlite.csproj", "{4A57961A-ED52-471F-BA21-EAEE9100E485}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Persistence.EFCore.SqlServer", "src\modules\Elsa.AI.Persistence.EFCore.SqlServer\Elsa.AI.Persistence.EFCore.SqlServer.csproj", "{6BE15A6D-F543-4084-BF6E-F00FD71F60F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Persistence.EFCore.PostgreSql", "src\modules\Elsa.AI.Persistence.EFCore.PostgreSql\Elsa.AI.Persistence.EFCore.PostgreSql.csproj", "{32F2EE33-8190-41D7-BD12-45AA02BEB930}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Persistence.EFCore.MySql", "src\modules\Elsa.AI.Persistence.EFCore.MySql\Elsa.AI.Persistence.EFCore.MySql.csproj", "{2D51FFE0-F114-48E1-BF58-FCED0FD1944F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.AI.Persistence.EFCore.Oracle", "src\modules\Elsa.AI.Persistence.EFCore.Oracle\Elsa.AI.Persistence.EFCore.Oracle.csproj", "{743C1835-E41E-4E5C-9D12-585BAEE59EEE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1887,6 +1897,66 @@ Global {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|x64.Build.0 = Release|Any CPU {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|x86.ActiveCfg = Release|Any CPU {DDEA0B6A-A615-4CF8-8B23-F93E390454B2}.Release|x86.Build.0 = Release|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Debug|x64.Build.0 = Debug|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Debug|x86.Build.0 = Debug|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Release|Any CPU.Build.0 = Release|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Release|x64.ActiveCfg = Release|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Release|x64.Build.0 = Release|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Release|x86.ActiveCfg = Release|Any CPU + {4A57961A-ED52-471F-BA21-EAEE9100E485}.Release|x86.Build.0 = Release|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Debug|x64.Build.0 = Debug|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Debug|x86.Build.0 = Debug|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Release|Any CPU.Build.0 = Release|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Release|x64.ActiveCfg = Release|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Release|x64.Build.0 = Release|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Release|x86.ActiveCfg = Release|Any CPU + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3}.Release|x86.Build.0 = Release|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Debug|x64.ActiveCfg = Debug|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Debug|x64.Build.0 = Debug|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Debug|x86.ActiveCfg = Debug|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Debug|x86.Build.0 = Debug|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Release|Any CPU.Build.0 = Release|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Release|x64.ActiveCfg = Release|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Release|x64.Build.0 = Release|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Release|x86.ActiveCfg = Release|Any CPU + {32F2EE33-8190-41D7-BD12-45AA02BEB930}.Release|x86.Build.0 = Release|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Debug|x64.ActiveCfg = Debug|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Debug|x64.Build.0 = Debug|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Debug|x86.ActiveCfg = Debug|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Debug|x86.Build.0 = Debug|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Release|Any CPU.Build.0 = Release|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Release|x64.ActiveCfg = Release|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Release|x64.Build.0 = Release|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Release|x86.ActiveCfg = Release|Any CPU + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F}.Release|x86.Build.0 = Release|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Debug|x64.ActiveCfg = Debug|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Debug|x64.Build.0 = Debug|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Debug|x86.ActiveCfg = Debug|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Debug|x86.Build.0 = Debug|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Release|Any CPU.Build.0 = Release|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Release|x64.ActiveCfg = Release|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Release|x64.Build.0 = Release|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Release|x86.ActiveCfg = Release|Any CPU + {743C1835-E41E-4E5C-9D12-585BAEE59EEE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1917,6 +1987,7 @@ Global {29638A67-E79F-44FE-AC05-DA499EBA929E} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912} {A516931E-EDBB-4FC3-BB94-1BB824D5BC61} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {BBCE36D1-6767-4ED1-B3E8-84D2567A962A} = {A516931E-EDBB-4FC3-BB94-1BB824D5BC61} + {73BBB1E3-AEEC-44E8-83D7-6DD9CD687FE3} = {9323DC40-29B4-4438-AAD7-C4D4069C072B} {B0946844-DE0E-4C59-9391-C0EB649DBF4E} = {18453B51-25EB-4317-A4B3-B10518252E92} {34BC9836-681D-43A5-BC39-E4FEE17FC528} = {08B41FFA-CEE3-46A7-B5C0-3EB65D37A16C} {CBB515F3-A0EF-43B5-A907-FD4E652DD66E} = {90031D64-CA0F-46D0-9AF4-8DC023A5FFCD} @@ -2020,21 +2091,12 @@ Global {E4B1546A-3F35-4B5C-815C-07E58B00163F} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} {E18931B2-82EE-4398-B38A-7B8B449957CD} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} {729FD075-2863-48AB-A3C0-A4F7A9277263} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {4FD4C59B-2804-4F6B-AD38-2562CC01C510} = {DD8D1712-B42C-4BF2-99AF-3F092E508C62} {157EDBA7-B04F-4EA0-8377-434D1065ACF6} = {18453B51-25EB-4317-A4B3-B10518252E92} + {27300F17-F959-4D4A-885F-FDDBE81456C1} = {DD8D1712-B42C-4BF2-99AF-3F092E508C62} {05AC1253-922A-4BC4-93D0-765752026B29} = {B08B4E00-C2AB-48F3-8389-449F42AEF179} {D1212632-20D4-4115-9E8F-05EEEC4604BE} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D} {215555CD-3B4A-46E7-ACE0-B4ADE1F0FE62} = {78FD90A4-90A5-445F-97F2-74BA835AFA5D} - {418472CD-1215-4962-9024-F26E9863B5A4} = {18453B51-25EB-4317-A4B3-B10518252E92} - {C597B963-A80F-4093-B3CC-9060854D9D38} = {18453B51-25EB-4317-A4B3-B10518252E92} - {E087B3BD-D579-422B-A29F-965D33E2C769} = {18453B51-25EB-4317-A4B3-B10518252E92} - {1C70A220-E69F-434B-966B-9C53A35B072B} = {18453B51-25EB-4317-A4B3-B10518252E92} - {9B74CCC3-9634-4A09-A270-CE1101D96824} = {18453B51-25EB-4317-A4B3-B10518252E92} - {DDEA0B6A-A615-4CF8-8B23-F93E390454B2} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} - {F52DF980-C8F7-49D5-A8A1-249B44223600} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC} = {F52DF980-C8F7-49D5-A8A1-249B44223600} - {C9E1429E-BC22-4ABC-820D-08709793ECE7} = {F52DF980-C8F7-49D5-A8A1-249B44223600} - {24CE63F9-29D1-4243-9144-BB2C1590FCC0} = {F52DF980-C8F7-49D5-A8A1-249B44223600} - {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5} = {F52DF980-C8F7-49D5-A8A1-249B44223600} {A5BE660B-BED0-4D72-BC60-2E6EB42061C0} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {86A93965-54B1-4983-9A87-6E3ADB584BED} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {E1F2B414-D9E4-4789-8138-3F9DA076FFB6} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} @@ -2044,11 +2106,24 @@ Global {6A17BD0C-8643-4684-BC3F-9E378A467605} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {2248A649-6CA4-4A1A-8BC3-34257238B560} = {3D0A6C71-4B96-411B-80DB-DDFAFF77C748} {807C6ACA-1C4B-4AC4-A60C-1FE6B9AB38AC} = {D3E5E9EF-DE26-41BF-A239-3241B5B5FD89} + {418472CD-1215-4962-9024-F26E9863B5A4} = {18453B51-25EB-4317-A4B3-B10518252E92} + {E8D76C93-4FE6-4B97-BC16-C8490CF3C8CC} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {C9E1429E-BC22-4ABC-820D-08709793ECE7} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {24CE63F9-29D1-4243-9144-BB2C1590FCC0} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {D3F07276-CD16-46BD-AEB1-8C7D6D7910C5} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {C597B963-A80F-4093-B3CC-9060854D9D38} = {18453B51-25EB-4317-A4B3-B10518252E92} + {E087B3BD-D579-422B-A29F-965D33E2C769} = {18453B51-25EB-4317-A4B3-B10518252E92} + {1C70A220-E69F-434B-966B-9C53A35B072B} = {18453B51-25EB-4317-A4B3-B10518252E92} + {9B74CCC3-9634-4A09-A270-CE1101D96824} = {18453B51-25EB-4317-A4B3-B10518252E92} + {DDEA0B6A-A615-4CF8-8B23-F93E390454B2} = {1B8D5897-902E-4632-8698-E89CAF3DDF54} + {F52DF980-C8F7-49D5-A8A1-249B44223600} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {9323DC40-29B4-4438-AAD7-C4D4069C072B} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {73BBB1E3-AEEC-44E8-83D7-6DD9CD687FE3} = {9323DC40-29B4-4438-AAD7-C4D4069C072B} {DD8D1712-B42C-4BF2-99AF-3F092E508C62} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} - {27300F17-F959-4D4A-885F-FDDBE81456C1} = {DD8D1712-B42C-4BF2-99AF-3F092E508C62} - {4FD4C59B-2804-4F6B-AD38-2562CC01C510} = {DD8D1712-B42C-4BF2-99AF-3F092E508C62} + {4A57961A-ED52-471F-BA21-EAEE9100E485} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {6BE15A6D-F543-4084-BF6E-F00FD71F60F3} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {32F2EE33-8190-41D7-BD12-45AA02BEB930} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {2D51FFE0-F114-48E1-BF58-FCED0FD1944F} = {F52DF980-C8F7-49D5-A8A1-249B44223600} + {743C1835-E41E-4E5C-9D12-585BAEE59EEE} = {F52DF980-C8F7-49D5-A8A1-249B44223600} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/DbContextFactories.cs b/src/modules/Elsa.AI.Persistence.EFCore.MySql/DbContextFactories.cs new file mode 100644 index 000000000..6b91e1ed3 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/DbContextFactories.cs @@ -0,0 +1,24 @@ +using Elsa.Persistence.EFCore.Abstractions; +using Elsa.Persistence.EFCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Elsa.AI.Persistence.EFCore.MySql; + +[UsedImplicitly] +public class AIDbContextFactory : MySqlAIDesignTimeDbContextFactory; + +public class MySqlAIDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase where TDbContext : DbContext +{ + protected override void ConfigureServices(IServiceCollection services) + { + services.AddMySqlEntityModelCreatingHandlers(); + } + + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaMySql(GetType().Assembly, connectionString, serverVersion: ServerVersion.Parse("9.0.0")); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/Elsa.AI.Persistence.EFCore.MySql.csproj b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Elsa.AI.Persistence.EFCore.MySql.csproj new file mode 100644 index 000000000..c6b540c4e --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Elsa.AI.Persistence.EFCore.MySql.csproj @@ -0,0 +1,19 @@ + + + + + net8.0;net9.0 + + Provides MySQL EF Core migrations for Elsa AI persistence. + + elsa module ai persistence efcore mysql + + + + + + + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/Extensions/MySqlAIPersistenceFeatureExtensions.cs b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Extensions/MySqlAIPersistenceFeatureExtensions.cs new file mode 100644 index 000000000..0b654b5d0 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Extensions/MySqlAIPersistenceFeatureExtensions.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using Elsa.AI.Persistence.EFCore.Features; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Elsa.AI.Persistence.EFCore.MySql.Extensions; + +public static class MySqlAIPersistenceFeatureExtensions +{ + private static Assembly Assembly => typeof(MySqlAIPersistenceFeatureExtensions).Assembly; + + public static AIPersistenceFeature UseMySql( + this AIPersistenceFeature feature, + string connectionString, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UseMySql(_ => connectionString, options, configure); + } + + public static AIPersistenceFeature UseMySql( + this AIPersistenceFeature feature, + Func connectionStringFunc, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UseMySql(Assembly, connectionStringFunc, options, configure); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/FodyWeavers.xml b/src/modules/Elsa.AI.Persistence.EFCore.MySql/FodyWeavers.xml new file mode 100644 index 000000000..8099e287a --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.Designer.cs b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.Designer.cs new file mode 100644 index 000000000..2013440fb --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.Designer.cs @@ -0,0 +1,208 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.MySql.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + [Migration("20260608124957_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ConversationId") + .HasColumnType("varchar(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProposalId") + .HasColumnType("varchar(255)"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("ToolInvocationId") + .HasColumnType("varchar(255)"); + + b.Property("TraceId") + .HasColumnType("longtext"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProviderSessionId") + .HasColumnType("longtext"); + + b.Property("RetentionExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AppliedAt") + .HasColumnType("datetime(6)"); + + b.Property("AppliedBy") + .HasColumnType("longtext"); + + b.Property("BaselineVersionId") + .HasColumnType("longtext"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("longtext"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("GraphDiff") + .HasColumnType("longtext"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedBy") + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.cs b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.cs new file mode 100644 index 000000000..4663e294b --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/20260608124957_Initial.cs @@ -0,0 +1,213 @@ +using System; +using Elsa.Persistence.EFCore; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.MySql.Migrations.AI +{ + /// + public partial class Initial : Migration + { + private readonly IElsaDbContextSchema _schema; + + public Initial(IElsaDbContextSchema schema) + { + _schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: _schema.Schema); + + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AIAuditRecords", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ActorId = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ConversationId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ProposalId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ToolInvocationId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Type = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Timestamp = table.Column(type: "datetime(6)", nullable: false), + TraceId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Summary = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Data = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AIAuditRecords", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AIConversations", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + UserId = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Title = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Status = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false), + ProviderSessionId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + RetentionMode = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + RetentionExpiresAt = table.Column(type: "datetime(6)", nullable: true), + Messages = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AIConversations", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "AIProposals", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TenantId = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ConversationId = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Kind = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Status = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + BaselineWorkflowDefinitionId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + BaselineVersionId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + WorkflowPayload = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Rationale = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Warnings = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + ValidationDiagnostics = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + GraphDiff = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedBy = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + ReviewedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ReviewedAt = table.Column(type: "datetime(6)", nullable: true), + AppliedBy = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + AppliedAt = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AIProposals", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ActorId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ActorId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ProposalId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ProposalId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_Timestamp", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ToolInvocationId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ToolInvocationId"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_RetentionExpiresAt", + schema: _schema.Schema, + table: "AIConversations", + column: "RetentionExpiresAt"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_Status", + schema: _schema.Schema, + table: "AIConversations", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_TenantId_UserId", + schema: _schema.Schema, + table: "AIConversations", + columns: new[] { "TenantId", "UserId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_Status", + schema: _schema.Schema, + table: "AIProposals", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIProposals", + columns: new[] { "TenantId", "ConversationId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AIAuditRecords", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIConversations", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIProposals", + schema: _schema.Schema); + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/AIDbContextModelSnapshot.cs b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/AIDbContextModelSnapshot.cs new file mode 100644 index 000000000..02637d768 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/Migrations/AI/AIDbContextModelSnapshot.cs @@ -0,0 +1,205 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.MySql.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + partial class AIDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ConversationId") + .HasColumnType("varchar(255)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProposalId") + .HasColumnType("varchar(255)"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("ToolInvocationId") + .HasColumnType("varchar(255)"); + + b.Property("TraceId") + .HasColumnType("longtext"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProviderSessionId") + .HasColumnType("longtext"); + + b.Property("RetentionExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AppliedAt") + .HasColumnType("datetime(6)"); + + b.Property("AppliedBy") + .HasColumnType("longtext"); + + b.Property("BaselineVersionId") + .HasColumnType("longtext"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("longtext"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("GraphDiff") + .HasColumnType("longtext"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReviewedBy") + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.MySql/ShellFeatures/MySqlAIPersistenceShellFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore.MySql/ShellFeatures/MySqlAIPersistenceShellFeature.cs new file mode 100644 index 000000000..c1d2f9d68 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.MySql/ShellFeatures/MySqlAIPersistenceShellFeature.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using CShells.Features; +using Elsa.AI.Host.ShellFeatures; +using Elsa.AI.Persistence.EFCore.ShellFeatures; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Platform.PackageManifest.Generator.Hints; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.AI.Persistence.EFCore.MySql.ShellFeatures; + +[ManifestFeatureCategory("AI")] +[ManifestFeatureCategory("Persistence")] +[ShellFeature( + DisplayName = "MySQL AI Persistence", + Description = "Provides MySQL persistence for Weaver AI conversations, proposals and audit records", + DependsOn = [typeof(AIFeature)])] +[UsedImplicitly] +[ManifestInfrastructure("mysql-database", "database", Reason = "Stores Weaver AI persistence data in MySQL.", Providers = new[] { "MySQL" }, ConfigurationKeys = new[] { "ConnectionString" })] +public class MySqlAIPersistenceShellFeature : EFCoreAIPersistenceShellFeatureBase +{ + protected override void ConfigureProvider(DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options) + { + builder.UseElsaMySql(migrationsAssembly, connectionString, options); + } + + protected override void OnConfiguring(IServiceCollection services) + { + services.AddMySqlEntityModelCreatingHandlers(); + base.OnConfiguring(services); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/DbContextFactories.cs b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/DbContextFactories.cs new file mode 100644 index 000000000..0f4235bc8 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/DbContextFactories.cs @@ -0,0 +1,19 @@ +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Abstractions; +using Elsa.Persistence.EFCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.AI.Persistence.EFCore.Oracle; + +[UsedImplicitly] +public class AIDbContextFactory : OracleAIDesignTimeDbContextFactory; + +public class OracleAIDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase where TDbContext : DbContext +{ + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + var options = new ElsaDbContextOptions().ConfigureOracle(); + builder.UseElsaOracle(GetType().Assembly, connectionString, options); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Elsa.AI.Persistence.EFCore.Oracle.csproj b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Elsa.AI.Persistence.EFCore.Oracle.csproj new file mode 100644 index 000000000..6732c4786 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Elsa.AI.Persistence.EFCore.Oracle.csproj @@ -0,0 +1,17 @@ + + + + + Provides Oracle EF Core migrations for Elsa AI persistence. + + elsa module ai persistence efcore oracle + + + + + + + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Extensions/OracleAIPersistenceFeatureExtensions.cs b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Extensions/OracleAIPersistenceFeatureExtensions.cs new file mode 100644 index 000000000..de866e027 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Extensions/OracleAIPersistenceFeatureExtensions.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using Elsa.AI.Persistence.EFCore.Features; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Oracle.EntityFrameworkCore.Infrastructure; + +namespace Elsa.AI.Persistence.EFCore.Oracle.Extensions; + +public static class OracleAIPersistenceFeatureExtensions +{ + private static Assembly Assembly => typeof(OracleAIPersistenceFeatureExtensions).Assembly; + + public static AIPersistenceFeature UseOracle( + this AIPersistenceFeature feature, + string connectionString, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UseOracle(_ => connectionString, options, configure); + } + + public static AIPersistenceFeature UseOracle( + this AIPersistenceFeature feature, + Func connectionStringFunc, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UseOracle(Assembly, connectionStringFunc, options, configure); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/FodyWeavers.xml b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/FodyWeavers.xml new file mode 100644 index 000000000..8099e287a --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.Designer.cs b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.Designer.cs new file mode 100644 index 000000000..af3ebe98e --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.Designer.cs @@ -0,0 +1,208 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Oracle.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.Oracle.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + [Migration("20260608125003_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ConversationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ProposalId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Timestamp") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("ToolInvocationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TraceId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ProviderSessionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("RetentionExpiresAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Title") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("UpdatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("AppliedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("AppliedBy") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("BaselineVersionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("GraphDiff") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ReviewedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("ReviewedBy") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.cs b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.cs new file mode 100644 index 000000000..8cd46f4d1 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/20260608125003_Initial.cs @@ -0,0 +1,174 @@ +using System; +using Elsa.Persistence.EFCore; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.Oracle.Migrations.AI +{ + /// + public partial class Initial : Migration + { + private readonly IElsaDbContextSchema _schema; + + public Initial(IElsaDbContextSchema schema) + { + _schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: _schema.Schema); + + migrationBuilder.CreateTable( + name: "AIAuditRecords", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "NVARCHAR2(450)", nullable: false), + TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true), + ActorId = table.Column(type: "NVARCHAR2(450)", nullable: false), + ConversationId = table.Column(type: "NVARCHAR2(450)", nullable: true), + ProposalId = table.Column(type: "NVARCHAR2(450)", nullable: true), + ToolInvocationId = table.Column(type: "NVARCHAR2(450)", nullable: true), + Type = table.Column(type: "NVARCHAR2(2000)", nullable: false), + Timestamp = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), + TraceId = table.Column(type: "NVARCHAR2(2000)", nullable: true), + Summary = table.Column(type: "NVARCHAR2(2000)", nullable: false), + Data = table.Column(type: "NVARCHAR2(2000)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIAuditRecords", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIConversations", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "NVARCHAR2(450)", nullable: false), + TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true), + UserId = table.Column(type: "NVARCHAR2(450)", nullable: false), + Title = table.Column(type: "NVARCHAR2(2000)", nullable: true), + Status = table.Column(type: "NVARCHAR2(450)", nullable: false), + CreatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), + UpdatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), + ProviderSessionId = table.Column(type: "NVARCHAR2(2000)", nullable: true), + RetentionMode = table.Column(type: "NVARCHAR2(2000)", nullable: false), + RetentionExpiresAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: true), + Messages = table.Column(type: "NVARCHAR2(2000)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIProposals", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "NVARCHAR2(450)", nullable: false), + TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true), + ConversationId = table.Column(type: "NVARCHAR2(450)", nullable: false), + Kind = table.Column(type: "NVARCHAR2(2000)", nullable: false), + Status = table.Column(type: "NVARCHAR2(450)", nullable: false), + BaselineWorkflowDefinitionId = table.Column(type: "NVARCHAR2(2000)", nullable: true), + BaselineVersionId = table.Column(type: "NVARCHAR2(2000)", nullable: true), + WorkflowPayload = table.Column(type: "NVARCHAR2(2000)", nullable: false), + Rationale = table.Column(type: "NVARCHAR2(2000)", nullable: false), + Warnings = table.Column(type: "NVARCHAR2(2000)", nullable: false), + ValidationDiagnostics = table.Column(type: "NVARCHAR2(2000)", nullable: false), + GraphDiff = table.Column(type: "NVARCHAR2(2000)", nullable: true), + CreatedBy = table.Column(type: "NVARCHAR2(2000)", nullable: false), + CreatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), + ReviewedBy = table.Column(type: "NVARCHAR2(2000)", nullable: true), + ReviewedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: true), + AppliedBy = table.Column(type: "NVARCHAR2(2000)", nullable: true), + AppliedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AIProposals", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ActorId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ActorId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ProposalId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ProposalId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_Timestamp", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ToolInvocationId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ToolInvocationId"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_RetentionExpiresAt", + schema: _schema.Schema, + table: "AIConversations", + column: "RetentionExpiresAt"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_Status", + schema: _schema.Schema, + table: "AIConversations", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_TenantId_UserId", + schema: _schema.Schema, + table: "AIConversations", + columns: new[] { "TenantId", "UserId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_Status", + schema: _schema.Schema, + table: "AIProposals", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIProposals", + columns: new[] { "TenantId", "ConversationId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AIAuditRecords", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIConversations", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIProposals", + schema: _schema.Schema); + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/AIDbContextModelSnapshot.cs b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/AIDbContextModelSnapshot.cs new file mode 100644 index 000000000..f2cfcb818 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/Migrations/AI/AIDbContextModelSnapshot.cs @@ -0,0 +1,205 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Oracle.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.Oracle.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + partial class AIDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ConversationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ProposalId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Timestamp") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("ToolInvocationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TraceId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ProviderSessionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("RetentionExpiresAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Title") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("UpdatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("AppliedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("AppliedBy") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("BaselineVersionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("GraphDiff") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ReviewedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("ReviewedBy") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Oracle/ShellFeatures/OracleAIPersistenceShellFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/ShellFeatures/OracleAIPersistenceShellFeature.cs new file mode 100644 index 000000000..b9ffb9710 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Oracle/ShellFeatures/OracleAIPersistenceShellFeature.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using CShells.Features; +using Elsa.AI.Host.ShellFeatures; +using Elsa.AI.Persistence.EFCore.ShellFeatures; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Platform.PackageManifest.Generator.Hints; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.AI.Persistence.EFCore.Oracle.ShellFeatures; + +[ManifestFeatureCategory("AI")] +[ManifestFeatureCategory("Persistence")] +[ShellFeature( + DisplayName = "Oracle AI Persistence", + Description = "Provides Oracle persistence for Weaver AI conversations, proposals and audit records", + DependsOn = [typeof(AIFeature)])] +[UsedImplicitly] +[ManifestInfrastructure("oracle-database", "database", Reason = "Stores Weaver AI persistence data in Oracle Database.", Providers = new[] { "Oracle" }, ConfigurationKeys = new[] { "ConnectionString" })] +public class OracleAIPersistenceShellFeature : EFCoreAIPersistenceShellFeatureBase +{ + protected override void ConfigureProvider(DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options) + { + options = options.ConfigureOracle(); + builder.UseElsaOracle(migrationsAssembly, connectionString, options); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/DbContextFactories.cs b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/DbContextFactories.cs new file mode 100644 index 000000000..6cbd2b631 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/DbContextFactories.cs @@ -0,0 +1,23 @@ +using Elsa.Persistence.EFCore.Abstractions; +using Elsa.Persistence.EFCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.AI.Persistence.EFCore.PostgreSql; + +[UsedImplicitly] +public class AIDbContextFactory : PostgreSqlAIDesignTimeDbContextFactory; + +public class PostgreSqlAIDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase where TDbContext : DbContext +{ + protected override void ConfigureServices(IServiceCollection services) + { + services.AddPostgreSqlEntityModelCreatingHandlers(); + } + + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaPostgreSql(GetType().Assembly, connectionString); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Elsa.AI.Persistence.EFCore.PostgreSql.csproj b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Elsa.AI.Persistence.EFCore.PostgreSql.csproj new file mode 100644 index 000000000..a0daddc59 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Elsa.AI.Persistence.EFCore.PostgreSql.csproj @@ -0,0 +1,17 @@ + + + + + Provides PostgreSQL EF Core migrations for Elsa AI persistence. + + elsa module ai persistence efcore postgresql + + + + + + + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Extensions/PostgreSqlAIPersistenceFeatureExtensions.cs b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Extensions/PostgreSqlAIPersistenceFeatureExtensions.cs new file mode 100644 index 000000000..1f23808d2 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Extensions/PostgreSqlAIPersistenceFeatureExtensions.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using Elsa.AI.Persistence.EFCore.Features; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; + +namespace Elsa.AI.Persistence.EFCore.PostgreSql.Extensions; + +public static class PostgreSqlAIPersistenceFeatureExtensions +{ + private static Assembly Assembly => typeof(PostgreSqlAIPersistenceFeatureExtensions).Assembly; + + public static AIPersistenceFeature UsePostgreSql( + this AIPersistenceFeature feature, + string connectionString, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UsePostgreSql(_ => connectionString, options, configure); + } + + public static AIPersistenceFeature UsePostgreSql( + this AIPersistenceFeature feature, + Func connectionStringFunc, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UsePostgreSql(Assembly, connectionStringFunc, options, configure); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/FodyWeavers.xml b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/FodyWeavers.xml new file mode 100644 index 000000000..8099e287a --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.Designer.cs b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.Designer.cs new file mode 100644 index 000000000..7576b2245 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.Designer.cs @@ -0,0 +1,208 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.PostgreSql.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + [Migration("20260608124950_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("text"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProposalId") + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ToolInvocationId") + .HasColumnType("text"); + + b.Property("TraceId") + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProviderSessionId") + .HasColumnType("text"); + + b.Property("RetentionExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AppliedBy") + .HasColumnType("text"); + + b.Property("BaselineVersionId") + .HasColumnType("text"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("text"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("GraphDiff") + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("text"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.cs b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.cs new file mode 100644 index 000000000..b5ded8767 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/20260608124950_Initial.cs @@ -0,0 +1,174 @@ +using System; +using Elsa.Persistence.EFCore; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.PostgreSql.Migrations.AI +{ + /// + public partial class Initial : Migration + { + private readonly IElsaDbContextSchema _schema; + + public Initial(IElsaDbContextSchema schema) + { + _schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: _schema.Schema); + + migrationBuilder.CreateTable( + name: "AIAuditRecords", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true), + ActorId = table.Column(type: "text", nullable: false), + ConversationId = table.Column(type: "text", nullable: true), + ProposalId = table.Column(type: "text", nullable: true), + ToolInvocationId = table.Column(type: "text", nullable: true), + Type = table.Column(type: "text", nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + TraceId = table.Column(type: "text", nullable: true), + Summary = table.Column(type: "text", nullable: false), + Data = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIAuditRecords", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIConversations", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: false), + Title = table.Column(type: "text", nullable: true), + Status = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ProviderSessionId = table.Column(type: "text", nullable: true), + RetentionMode = table.Column(type: "text", nullable: false), + RetentionExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + Messages = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIProposals", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + TenantId = table.Column(type: "text", nullable: true), + ConversationId = table.Column(type: "text", nullable: false), + Kind = table.Column(type: "text", nullable: false), + Status = table.Column(type: "text", nullable: false), + BaselineWorkflowDefinitionId = table.Column(type: "text", nullable: true), + BaselineVersionId = table.Column(type: "text", nullable: true), + WorkflowPayload = table.Column(type: "text", nullable: false), + Rationale = table.Column(type: "text", nullable: false), + Warnings = table.Column(type: "text", nullable: false), + ValidationDiagnostics = table.Column(type: "text", nullable: false), + GraphDiff = table.Column(type: "text", nullable: true), + CreatedBy = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ReviewedBy = table.Column(type: "text", nullable: true), + ReviewedAt = table.Column(type: "timestamp with time zone", nullable: true), + AppliedBy = table.Column(type: "text", nullable: true), + AppliedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AIProposals", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ActorId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ActorId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ProposalId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ProposalId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_Timestamp", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ToolInvocationId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ToolInvocationId"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_RetentionExpiresAt", + schema: _schema.Schema, + table: "AIConversations", + column: "RetentionExpiresAt"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_Status", + schema: _schema.Schema, + table: "AIConversations", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_TenantId_UserId", + schema: _schema.Schema, + table: "AIConversations", + columns: new[] { "TenantId", "UserId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_Status", + schema: _schema.Schema, + table: "AIProposals", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIProposals", + columns: new[] { "TenantId", "ConversationId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AIAuditRecords", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIConversations", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIProposals", + schema: _schema.Schema); + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/AIDbContextModelSnapshot.cs b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/AIDbContextModelSnapshot.cs new file mode 100644 index 000000000..869be3fa1 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/Migrations/AI/AIDbContextModelSnapshot.cs @@ -0,0 +1,205 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.PostgreSql.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + partial class AIDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("text"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProposalId") + .HasColumnType("text"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ToolInvocationId") + .HasColumnType("text"); + + b.Property("TraceId") + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProviderSessionId") + .HasColumnType("text"); + + b.Property("RetentionExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppliedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AppliedBy") + .HasColumnType("text"); + + b.Property("BaselineVersionId") + .HasColumnType("text"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("text"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("GraphDiff") + .HasColumnType("text"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("text"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReviewedBy") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("text"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/ShellFeatures/PostgreSqlAIPersistenceShellFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/ShellFeatures/PostgreSqlAIPersistenceShellFeature.cs new file mode 100644 index 000000000..eb16efeae --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.PostgreSql/ShellFeatures/PostgreSqlAIPersistenceShellFeature.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using CShells.Features; +using Elsa.AI.Host.ShellFeatures; +using Elsa.AI.Persistence.EFCore.ShellFeatures; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Platform.PackageManifest.Generator.Hints; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.AI.Persistence.EFCore.PostgreSql.ShellFeatures; + +[ManifestFeatureCategory("AI")] +[ManifestFeatureCategory("Persistence")] +[ShellFeature( + DisplayName = "PostgreSQL AI Persistence", + Description = "Provides PostgreSQL persistence for Weaver AI conversations, proposals and audit records", + DependsOn = [typeof(AIFeature)])] +[UsedImplicitly] +[ManifestInfrastructure("postgresql-database", "database", Reason = "Stores Weaver AI persistence data in PostgreSQL.", Providers = new[] { "PostgreSQL" }, ConfigurationKeys = new[] { "ConnectionString" })] +public class PostgreSqlAIPersistenceShellFeature : EFCoreAIPersistenceShellFeatureBase +{ + protected override void ConfigureProvider(DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options) + { + builder.UseElsaPostgreSql(migrationsAssembly, connectionString, options); + } + + protected override void OnConfiguring(IServiceCollection services) + { + services.AddPostgreSqlEntityModelCreatingHandlers(); + base.OnConfiguring(services); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/DbContextFactories.cs b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/DbContextFactories.cs new file mode 100644 index 000000000..a316a1154 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/DbContextFactories.cs @@ -0,0 +1,17 @@ +using Elsa.Persistence.EFCore.Abstractions; +using Elsa.Persistence.EFCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.AI.Persistence.EFCore.SqlServer; + +[UsedImplicitly] +public class AIDbContextFactory : SqlServerAIDesignTimeDbContextFactory; + +public class SqlServerAIDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase where TDbContext : DbContext +{ + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaSqlServer(GetType().Assembly, connectionString); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Elsa.AI.Persistence.EFCore.SqlServer.csproj b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Elsa.AI.Persistence.EFCore.SqlServer.csproj new file mode 100644 index 000000000..8d3efa587 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Elsa.AI.Persistence.EFCore.SqlServer.csproj @@ -0,0 +1,17 @@ + + + + + Provides SQL Server EF Core migrations for Elsa AI persistence. + + elsa module ai persistence efcore sqlserver + + + + + + + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Extensions/SqlServerAIPersistenceFeatureExtensions.cs b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Extensions/SqlServerAIPersistenceFeatureExtensions.cs new file mode 100644 index 000000000..ac8581cce --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Extensions/SqlServerAIPersistenceFeatureExtensions.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using Elsa.AI.Persistence.EFCore.Features; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Elsa.AI.Persistence.EFCore.SqlServer.Extensions; + +public static class SqlServerAIPersistenceFeatureExtensions +{ + private static Assembly Assembly => typeof(SqlServerAIPersistenceFeatureExtensions).Assembly; + + public static AIPersistenceFeature UseSqlServer( + this AIPersistenceFeature feature, + string connectionString, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UseSqlServer(_ => connectionString, options, configure); + } + + public static AIPersistenceFeature UseSqlServer( + this AIPersistenceFeature feature, + Func connectionStringFunc, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UseSqlServer(Assembly, connectionStringFunc, options, configure); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/FodyWeavers.xml b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/FodyWeavers.xml new file mode 100644 index 000000000..8099e287a --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.Designer.cs b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.Designer.cs new file mode 100644 index 000000000..7915869aa --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.Designer.cs @@ -0,0 +1,208 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.SqlServer.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + [Migration("20260608124944_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ConversationId") + .HasColumnType("nvarchar(450)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProposalId") + .HasColumnType("nvarchar(450)"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("ToolInvocationId") + .HasColumnType("nvarchar(450)"); + + b.Property("TraceId") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProviderSessionId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetentionExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AppliedAt") + .HasColumnType("datetimeoffset"); + + b.Property("AppliedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("BaselineVersionId") + .HasColumnType("nvarchar(max)"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("nvarchar(max)"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GraphDiff") + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.cs b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.cs new file mode 100644 index 000000000..d480e0569 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/20260608124944_Initial.cs @@ -0,0 +1,174 @@ +using System; +using Elsa.Persistence.EFCore; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.SqlServer.Migrations.AI +{ + /// + public partial class Initial : Migration + { + private readonly IElsaDbContextSchema _schema; + + public Initial(IElsaDbContextSchema schema) + { + _schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: _schema.Schema); + + migrationBuilder.CreateTable( + name: "AIAuditRecords", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true), + ActorId = table.Column(type: "nvarchar(450)", nullable: false), + ConversationId = table.Column(type: "nvarchar(450)", nullable: true), + ProposalId = table.Column(type: "nvarchar(450)", nullable: true), + ToolInvocationId = table.Column(type: "nvarchar(450)", nullable: true), + Type = table.Column(type: "nvarchar(max)", nullable: false), + Timestamp = table.Column(type: "datetimeoffset", nullable: false), + TraceId = table.Column(type: "nvarchar(max)", nullable: true), + Summary = table.Column(type: "nvarchar(max)", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIAuditRecords", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIConversations", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + Title = table.Column(type: "nvarchar(max)", nullable: true), + Status = table.Column(type: "nvarchar(450)", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + UpdatedAt = table.Column(type: "datetimeoffset", nullable: false), + ProviderSessionId = table.Column(type: "nvarchar(max)", nullable: true), + RetentionMode = table.Column(type: "nvarchar(max)", nullable: false), + RetentionExpiresAt = table.Column(type: "datetimeoffset", nullable: true), + Messages = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIProposals", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + TenantId = table.Column(type: "nvarchar(450)", nullable: true), + ConversationId = table.Column(type: "nvarchar(450)", nullable: false), + Kind = table.Column(type: "nvarchar(max)", nullable: false), + Status = table.Column(type: "nvarchar(450)", nullable: false), + BaselineWorkflowDefinitionId = table.Column(type: "nvarchar(max)", nullable: true), + BaselineVersionId = table.Column(type: "nvarchar(max)", nullable: true), + WorkflowPayload = table.Column(type: "nvarchar(max)", nullable: false), + Rationale = table.Column(type: "nvarchar(max)", nullable: false), + Warnings = table.Column(type: "nvarchar(max)", nullable: false), + ValidationDiagnostics = table.Column(type: "nvarchar(max)", nullable: false), + GraphDiff = table.Column(type: "nvarchar(max)", nullable: true), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + ReviewedBy = table.Column(type: "nvarchar(max)", nullable: true), + ReviewedAt = table.Column(type: "datetimeoffset", nullable: true), + AppliedBy = table.Column(type: "nvarchar(max)", nullable: true), + AppliedAt = table.Column(type: "datetimeoffset", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AIProposals", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ActorId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ActorId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ProposalId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ProposalId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_Timestamp", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ToolInvocationId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ToolInvocationId"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_RetentionExpiresAt", + schema: _schema.Schema, + table: "AIConversations", + column: "RetentionExpiresAt"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_Status", + schema: _schema.Schema, + table: "AIConversations", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_TenantId_UserId", + schema: _schema.Schema, + table: "AIConversations", + columns: new[] { "TenantId", "UserId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_Status", + schema: _schema.Schema, + table: "AIProposals", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIProposals", + columns: new[] { "TenantId", "ConversationId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AIAuditRecords", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIConversations", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIProposals", + schema: _schema.Schema); + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/AIDbContextModelSnapshot.cs b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/AIDbContextModelSnapshot.cs new file mode 100644 index 000000000..5ea3babc6 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/Migrations/AI/AIDbContextModelSnapshot.cs @@ -0,0 +1,205 @@ +// +using System; +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.SqlServer.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + partial class AIDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ConversationId") + .HasColumnType("nvarchar(450)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProposalId") + .HasColumnType("nvarchar(450)"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("ToolInvocationId") + .HasColumnType("nvarchar(450)"); + + b.Property("TraceId") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProviderSessionId") + .HasColumnType("nvarchar(max)"); + + b.Property("RetentionExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AppliedAt") + .HasColumnType("datetimeoffset"); + + b.Property("AppliedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("BaselineVersionId") + .HasColumnType("nvarchar(max)"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("nvarchar(max)"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GraphDiff") + .HasColumnType("nvarchar(max)"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReviewedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ReviewedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/ShellFeatures/SqlServerAIPersistenceShellFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/ShellFeatures/SqlServerAIPersistenceShellFeature.cs new file mode 100644 index 000000000..e9a34ab41 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.SqlServer/ShellFeatures/SqlServerAIPersistenceShellFeature.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using CShells.Features; +using Elsa.AI.Host.ShellFeatures; +using Elsa.AI.Persistence.EFCore.ShellFeatures; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Platform.PackageManifest.Generator.Hints; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.AI.Persistence.EFCore.SqlServer.ShellFeatures; + +[ManifestFeatureCategory("AI")] +[ManifestFeatureCategory("Persistence")] +[ShellFeature( + DisplayName = "SQL Server AI Persistence", + Description = "Provides SQL Server persistence for Weaver AI conversations, proposals and audit records", + DependsOn = [typeof(AIFeature)])] +[UsedImplicitly] +[ManifestInfrastructure("sqlserver-database", "database", Reason = "Stores Weaver AI persistence data in SQL Server.", Providers = new[] { "SQL Server" }, ConfigurationKeys = new[] { "ConnectionString" })] +public class SqlServerAIPersistenceShellFeature : EFCoreAIPersistenceShellFeatureBase +{ + protected override void ConfigureProvider(DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options) + { + builder.UseElsaSqlServer(migrationsAssembly, connectionString, options); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/DbContextFactories.cs b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/DbContextFactories.cs new file mode 100644 index 000000000..b1be56e49 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/DbContextFactories.cs @@ -0,0 +1,23 @@ +using Elsa.Persistence.EFCore.Abstractions; +using Elsa.Persistence.EFCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.AI.Persistence.EFCore.Sqlite; + +[UsedImplicitly] +public class AIDbContextFactory : SqliteAIDesignTimeDbContextFactory; + +public class SqliteAIDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase where TDbContext : DbContext +{ + protected override void ConfigureServices(IServiceCollection services) + { + services.AddSqliteEntityModelCreatingHandlers(); + } + + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaSqlite(GetType().Assembly, connectionString); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Elsa.AI.Persistence.EFCore.Sqlite.csproj b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Elsa.AI.Persistence.EFCore.Sqlite.csproj new file mode 100644 index 000000000..537f21eeb --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Elsa.AI.Persistence.EFCore.Sqlite.csproj @@ -0,0 +1,17 @@ + + + + + Provides SQLite EF Core migrations for Elsa AI persistence. + + elsa module ai persistence efcore sqlite + + + + + + + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Extensions/SqliteAIPersistenceFeatureExtensions.cs b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Extensions/SqliteAIPersistenceFeatureExtensions.cs new file mode 100644 index 000000000..ddd589d57 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Extensions/SqliteAIPersistenceFeatureExtensions.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using Elsa.AI.Persistence.EFCore.Features; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Elsa.AI.Persistence.EFCore.Sqlite.Extensions; + +public static class SqliteAIPersistenceFeatureExtensions +{ + private static Assembly Assembly => typeof(SqliteAIPersistenceFeatureExtensions).Assembly; + + public static AIPersistenceFeature UseSqlite( + this AIPersistenceFeature feature, + string? connectionString = null, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + connectionString ??= "Data Source=elsa.sqlite.db;Cache=Shared;"; + return feature.UseSqlite(_ => connectionString, options, configure); + } + + public static AIPersistenceFeature UseSqlite( + this AIPersistenceFeature feature, + Func connectionStringFunc, + ElsaDbContextOptions? options = null, + Action? configure = null) + { + return feature.UseSqlite(Assembly, connectionStringFunc, options, configure); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/FodyWeavers.xml b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/FodyWeavers.xml new file mode 100644 index 000000000..8099e287a --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.Designer.cs b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.Designer.cs new file mode 100644 index 000000000..39d68f4f4 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.Designer.cs @@ -0,0 +1,207 @@ +// +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.Sqlite.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + [Migration("20260608124921_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16"); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProposalId") + .HasColumnType("TEXT"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ToolInvocationId") + .HasColumnType("TEXT"); + + b.Property("TraceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProviderSessionId") + .HasColumnType("TEXT"); + + b.Property("RetentionExpiresAt") + .HasColumnType("TEXT"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AppliedAt") + .HasColumnType("TEXT"); + + b.Property("AppliedBy") + .HasColumnType("TEXT"); + + b.Property("BaselineVersionId") + .HasColumnType("TEXT"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GraphDiff") + .HasColumnType("TEXT"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReviewedAt") + .HasColumnType("TEXT"); + + b.Property("ReviewedBy") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.cs b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.cs new file mode 100644 index 000000000..11b0ab9f0 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/20260608124921_Initial.cs @@ -0,0 +1,174 @@ +using System; +using Elsa.Persistence.EFCore; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.Sqlite.Migrations.AI +{ + /// + public partial class Initial : Migration + { + private readonly IElsaDbContextSchema _schema; + + public Initial(IElsaDbContextSchema schema) + { + _schema = schema ?? throw new ArgumentNullException(nameof(schema)); + } + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: _schema.Schema); + + migrationBuilder.CreateTable( + name: "AIAuditRecords", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true), + ActorId = table.Column(type: "TEXT", nullable: false), + ConversationId = table.Column(type: "TEXT", nullable: true), + ProposalId = table.Column(type: "TEXT", nullable: true), + ToolInvocationId = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "TEXT", nullable: false), + Timestamp = table.Column(type: "TEXT", nullable: false), + TraceId = table.Column(type: "TEXT", nullable: true), + Summary = table.Column(type: "TEXT", nullable: false), + Data = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIAuditRecords", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIConversations", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + ProviderSessionId = table.Column(type: "TEXT", nullable: true), + RetentionMode = table.Column(type: "TEXT", nullable: false), + RetentionExpiresAt = table.Column(type: "TEXT", nullable: true), + Messages = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AIConversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AIProposals", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true), + ConversationId = table.Column(type: "TEXT", nullable: false), + Kind = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "TEXT", nullable: false), + BaselineWorkflowDefinitionId = table.Column(type: "TEXT", nullable: true), + BaselineVersionId = table.Column(type: "TEXT", nullable: true), + WorkflowPayload = table.Column(type: "TEXT", nullable: false), + Rationale = table.Column(type: "TEXT", nullable: false), + Warnings = table.Column(type: "TEXT", nullable: false), + ValidationDiagnostics = table.Column(type: "TEXT", nullable: false), + GraphDiff = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ReviewedBy = table.Column(type: "TEXT", nullable: true), + ReviewedAt = table.Column(type: "TEXT", nullable: true), + AppliedBy = table.Column(type: "TEXT", nullable: true), + AppliedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AIProposals", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ActorId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ActorId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ProposalId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ProposalId"); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "ConversationId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_TenantId_Timestamp", + schema: _schema.Schema, + table: "AIAuditRecords", + columns: new[] { "TenantId", "Timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_AIAuditRecords_ToolInvocationId", + schema: _schema.Schema, + table: "AIAuditRecords", + column: "ToolInvocationId"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_RetentionExpiresAt", + schema: _schema.Schema, + table: "AIConversations", + column: "RetentionExpiresAt"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_Status", + schema: _schema.Schema, + table: "AIConversations", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIConversations_TenantId_UserId", + schema: _schema.Schema, + table: "AIConversations", + columns: new[] { "TenantId", "UserId" }); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_Status", + schema: _schema.Schema, + table: "AIProposals", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_AIProposals_TenantId_ConversationId", + schema: _schema.Schema, + table: "AIProposals", + columns: new[] { "TenantId", "ConversationId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AIAuditRecords", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIConversations", + schema: _schema.Schema); + + migrationBuilder.DropTable( + name: "AIProposals", + schema: _schema.Schema); + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/AIDbContextModelSnapshot.cs b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/AIDbContextModelSnapshot.cs new file mode 100644 index 000000000..86e9d7b12 --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/Migrations/AI/AIDbContextModelSnapshot.cs @@ -0,0 +1,204 @@ +// +using Elsa.AI.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.AI.Persistence.EFCore.Sqlite.Migrations.AI +{ + [DbContext(typeof(AIDbContext))] + partial class AIDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.16"); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActorId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProposalId") + .HasColumnType("TEXT"); + + b.Property("Summary") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ToolInvocationId") + .HasColumnType("TEXT"); + + b.Property("TraceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActorId"); + + b.HasIndex("ProposalId"); + + b.HasIndex("ToolInvocationId"); + + b.HasIndex("TenantId", "ConversationId"); + + b.HasIndex("TenantId", "Timestamp"); + + b.ToTable("AIAuditRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Messages") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProviderSessionId") + .HasColumnType("TEXT"); + + b.Property("RetentionExpiresAt") + .HasColumnType("TEXT"); + + b.Property("RetentionMode") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RetentionExpiresAt"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AIConversations", "Elsa"); + }); + + modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AppliedAt") + .HasColumnType("TEXT"); + + b.Property("AppliedBy") + .HasColumnType("TEXT"); + + b.Property("BaselineVersionId") + .HasColumnType("TEXT"); + + b.Property("BaselineWorkflowDefinitionId") + .HasColumnType("TEXT"); + + b.Property("ConversationId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GraphDiff") + .HasColumnType("TEXT"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Rationale") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReviewedAt") + .HasColumnType("TEXT"); + + b.Property("ReviewedBy") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("ValidationDiagnostics") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Warnings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowPayload") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("TenantId", "ConversationId"); + + b.ToTable("AIProposals", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/ShellFeatures/SqliteAIPersistenceShellFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/ShellFeatures/SqliteAIPersistenceShellFeature.cs new file mode 100644 index 000000000..bf80c46fc --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore.Sqlite/ShellFeatures/SqliteAIPersistenceShellFeature.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using CShells.Features; +using Elsa.AI.Host.ShellFeatures; +using Elsa.AI.Persistence.EFCore.ShellFeatures; +using Elsa.Persistence.EFCore; +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Platform.PackageManifest.Generator.Hints; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.AI.Persistence.EFCore.Sqlite.ShellFeatures; + +[ManifestFeatureCategory("AI")] +[ManifestFeatureCategory("Persistence")] +[ShellFeature( + DisplayName = "SQLite AI Persistence", + Description = "Provides SQLite persistence for Weaver AI conversations, proposals and audit records", + DependsOn = [typeof(AIFeature)])] +[UsedImplicitly] +[ManifestInfrastructure("sqlite-database", "database", Reason = "Stores Weaver AI persistence data in SQLite.", Providers = new[] { "SQLite" }, ConfigurationKeys = new[] { "ConnectionString" })] +public class SqliteAIPersistenceShellFeature : EFCoreAIPersistenceShellFeatureBase +{ + protected override void ConfigureProvider(DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options) + { + builder.UseElsaSqlite(migrationsAssembly, connectionString, options); + } + + protected override void OnConfiguring(IServiceCollection services) + { + services.AddSqliteEntityModelCreatingHandlers(); + base.OnConfiguring(services); + } +} diff --git a/src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs b/src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs index 6b8a70377..9bef9f1df 100644 --- a/src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs +++ b/src/modules/Elsa.AI.Persistence.EFCore/AIDbContext.cs @@ -1,9 +1,21 @@ using Elsa.AI.Persistence.EFCore.Entities; +using Elsa.Persistence.EFCore; +using Microsoft.Extensions.DependencyInjection; namespace Elsa.AI.Persistence.EFCore; -public class AIDbContext(DbContextOptions options) : DbContext(options) +public class AIDbContext : ElsaDbContextBase { + private static readonly IServiceProvider EmptyServiceProvider = new ServiceCollection().BuildServiceProvider(); + + public AIDbContext(DbContextOptions options) : this(options, EmptyServiceProvider) + { + } + + public AIDbContext(DbContextOptions options, IServiceProvider serviceProvider) : base(options, serviceProvider) + { + } + public DbSet Conversations => Set(); public DbSet Proposals => Set(); public DbSet AuditRecords => Set(); @@ -46,5 +58,7 @@ public class AIDbContext(DbContextOptions options) : DbContext(opti entity.Property(x => x.RetentionMode).IsRequired(); entity.Property(x => x.Messages).IsRequired(); }); + + base.OnModelCreating(modelBuilder); } } diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj b/src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj index 1a0e0ce8f..89d199a6b 100644 --- a/src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj +++ b/src/modules/Elsa.AI.Persistence.EFCore/Elsa.AI.Persistence.EFCore.csproj @@ -11,11 +11,11 @@ - + diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.AI.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs index 89f5aee95..4f8fc70e2 100644 --- a/src/modules/Elsa.AI.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/Elsa.AI.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs @@ -23,7 +23,13 @@ public static class ServiceCollectionExtensions } services.TryAddScoped(); + services.AddAIPersistenceStoreServices(); + return services; + } + + internal static IServiceCollection AddAIPersistenceStoreServices(this IServiceCollection services) + { services.Replace(ServiceDescriptor.Scoped()); services.Replace(ServiceDescriptor.Scoped()); services.TryAddEnumerable(ServiceDescriptor.Scoped()); diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Features/AIPersistenceFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore/Features/AIPersistenceFeature.cs index 4e4f0dd78..e8119e853 100644 --- a/src/modules/Elsa.AI.Persistence.EFCore/Features/AIPersistenceFeature.cs +++ b/src/modules/Elsa.AI.Persistence.EFCore/Features/AIPersistenceFeature.cs @@ -1,16 +1,22 @@ using Elsa.Extensions; -using Elsa.Features.Abstractions; using Elsa.Features.Services; +using Elsa.Persistence.EFCore; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace Elsa.AI.Persistence.EFCore.Features; -public class AIPersistenceFeature(IModule module) : FeatureBase(module) +public class AIPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { public Action? ConfigureDbContext { get; set; } public override void Apply() { - Services.AddAIPersistenceStores(ConfigureDbContext); + if (DbContextOptionsBuilder == null && ConfigureDbContext != null) + DbContextOptionsBuilder = (_, builder) => ConfigureDbContext(builder); + + base.Apply(); + Services.AddScoped(sp => sp.GetRequiredService>().CreateDbContext()); + Services.AddAIPersistenceStoreServices(); } } diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.Designer.cs b/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.Designer.cs deleted file mode 100644 index dc9b08e71..000000000 --- a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.Designer.cs +++ /dev/null @@ -1,158 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Elsa.AI.Persistence.EFCore.Migrations; - -[DbContext(typeof(AIDbContext))] -[Migration("20260521002000_Initial")] -partial class Initial -{ - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.16"); - - modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => - { - b.Property("Id"); - - b.Property("ActorId") - .IsRequired(); - - b.Property("ConversationId"); - - b.Property("Data") - .IsRequired(); - - b.Property("ProposalId"); - - b.Property("Summary") - .IsRequired(); - - b.Property("TenantId"); - - b.Property("Timestamp"); - - b.Property("ToolInvocationId"); - - b.Property("TraceId"); - - b.Property("Type") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("ActorId"); - - b.HasIndex("ProposalId"); - - b.HasIndex("ToolInvocationId"); - - b.HasIndex("TenantId", "ConversationId"); - - b.HasIndex("TenantId", "Timestamp"); - - b.ToTable("AIAuditRecords"); - }); - - modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => - { - b.Property("Id"); - - b.Property("CreatedAt"); - - b.Property("Messages") - .IsRequired(); - - b.Property("ProviderSessionId"); - - b.Property("RetentionExpiresAt"); - - b.Property("RetentionMode") - .IsRequired(); - - b.Property("Status") - .IsRequired(); - - b.Property("TenantId"); - - b.Property("Title"); - - b.Property("UpdatedAt"); - - b.Property("UserId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("RetentionExpiresAt"); - - b.HasIndex("Status"); - - b.HasIndex("TenantId", "UserId"); - - b.ToTable("AIConversations"); - }); - - modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => - { - b.Property("Id"); - - b.Property("AppliedBy"); - - b.Property("AppliedAt"); - - b.Property("BaselineVersionId"); - - b.Property("BaselineWorkflowDefinitionId"); - - b.Property("ConversationId") - .IsRequired(); - - b.Property("CreatedAt"); - - b.Property("CreatedBy") - .IsRequired(); - - b.Property("GraphDiff"); - - b.Property("Kind") - .IsRequired(); - - b.Property("Rationale") - .IsRequired(); - - b.Property("ReviewedBy"); - - b.Property("ReviewedAt"); - - b.Property("Status") - .IsRequired(); - - b.Property("TenantId"); - - b.Property("ValidationDiagnostics") - .IsRequired(); - - b.Property("Warnings") - .IsRequired(); - - b.Property("WorkflowPayload") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("Status"); - - b.HasIndex("TenantId", "ConversationId"); - - b.ToTable("AIProposals"); - }); -#pragma warning restore 612, 618 - } -} diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.cs b/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.cs deleted file mode 100644 index 6053c5cb2..000000000 --- a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/20260521002000_Initial.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Elsa.AI.Persistence.EFCore.Migrations; - -public partial class Initial : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AIAuditRecords", - columns: table => new - { - Id = table.Column(nullable: false), - TenantId = table.Column(nullable: true), - ActorId = table.Column(nullable: false), - ConversationId = table.Column(nullable: true), - ProposalId = table.Column(nullable: true), - ToolInvocationId = table.Column(nullable: true), - Type = table.Column(nullable: false), - Timestamp = table.Column(nullable: false), - TraceId = table.Column(nullable: true), - Summary = table.Column(nullable: false), - Data = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AIAuditRecords", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AIConversations", - columns: table => new - { - Id = table.Column(nullable: false), - TenantId = table.Column(nullable: true), - UserId = table.Column(nullable: false), - Title = table.Column(nullable: true), - Status = table.Column(nullable: false), - CreatedAt = table.Column(nullable: false), - UpdatedAt = table.Column(nullable: false), - ProviderSessionId = table.Column(nullable: true), - RetentionMode = table.Column(nullable: false), - RetentionExpiresAt = table.Column(nullable: true), - Messages = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AIConversations", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AIProposals", - columns: table => new - { - Id = table.Column(nullable: false), - TenantId = table.Column(nullable: true), - ConversationId = table.Column(nullable: false), - Kind = table.Column(nullable: false), - Status = table.Column(nullable: false), - BaselineWorkflowDefinitionId = table.Column(nullable: true), - BaselineVersionId = table.Column(nullable: true), - WorkflowPayload = table.Column(nullable: false), - Rationale = table.Column(nullable: false), - Warnings = table.Column(nullable: false), - ValidationDiagnostics = table.Column(nullable: false), - GraphDiff = table.Column(nullable: true), - CreatedBy = table.Column(nullable: false), - CreatedAt = table.Column(nullable: false), - ReviewedBy = table.Column(nullable: true), - ReviewedAt = table.Column(nullable: true), - AppliedBy = table.Column(nullable: true), - AppliedAt = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AIProposals", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_AIAuditRecords_ProposalId", - table: "AIAuditRecords", - column: "ProposalId"); - - migrationBuilder.CreateIndex( - name: "IX_AIAuditRecords_ActorId", - table: "AIAuditRecords", - column: "ActorId"); - - migrationBuilder.CreateIndex( - name: "IX_AIAuditRecords_TenantId_Timestamp", - table: "AIAuditRecords", - columns: new[] { "TenantId", "Timestamp" }); - - migrationBuilder.CreateIndex( - name: "IX_AIAuditRecords_TenantId_ConversationId", - table: "AIAuditRecords", - columns: new[] { "TenantId", "ConversationId" }); - - migrationBuilder.CreateIndex( - name: "IX_AIAuditRecords_ToolInvocationId", - table: "AIAuditRecords", - column: "ToolInvocationId"); - - migrationBuilder.CreateIndex( - name: "IX_AIConversations_RetentionExpiresAt", - table: "AIConversations", - column: "RetentionExpiresAt"); - - migrationBuilder.CreateIndex( - name: "IX_AIConversations_Status", - table: "AIConversations", - column: "Status"); - - migrationBuilder.CreateIndex( - name: "IX_AIConversations_TenantId_UserId", - table: "AIConversations", - columns: new[] { "TenantId", "UserId" }); - - migrationBuilder.CreateIndex( - name: "IX_AIProposals_Status", - table: "AIProposals", - column: "Status"); - - migrationBuilder.CreateIndex( - name: "IX_AIProposals_TenantId_ConversationId", - table: "AIProposals", - columns: new[] { "TenantId", "ConversationId" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "AIAuditRecords"); - migrationBuilder.DropTable(name: "AIConversations"); - migrationBuilder.DropTable(name: "AIProposals"); - } -} diff --git a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/AIDbContextModelSnapshot.cs b/src/modules/Elsa.AI.Persistence.EFCore/Migrations/AIDbContextModelSnapshot.cs deleted file mode 100644 index 42f24593c..000000000 --- a/src/modules/Elsa.AI.Persistence.EFCore/Migrations/AIDbContextModelSnapshot.cs +++ /dev/null @@ -1,156 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -#nullable disable - -namespace Elsa.AI.Persistence.EFCore.Migrations; - -[DbContext(typeof(AIDbContext))] -partial class AIDbContextModelSnapshot : ModelSnapshot -{ - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.16"); - - modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIAuditRecord", b => - { - b.Property("Id"); - - b.Property("ActorId") - .IsRequired(); - - b.Property("ConversationId"); - - b.Property("Data") - .IsRequired(); - - b.Property("ProposalId"); - - b.Property("Summary") - .IsRequired(); - - b.Property("TenantId"); - - b.Property("Timestamp"); - - b.Property("ToolInvocationId"); - - b.Property("TraceId"); - - b.Property("Type") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("ActorId"); - - b.HasIndex("ProposalId"); - - b.HasIndex("ToolInvocationId"); - - b.HasIndex("TenantId", "ConversationId"); - - b.HasIndex("TenantId", "Timestamp"); - - b.ToTable("AIAuditRecords"); - }); - - modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIConversationRecord", b => - { - b.Property("Id"); - - b.Property("CreatedAt"); - - b.Property("Messages") - .IsRequired(); - - b.Property("ProviderSessionId"); - - b.Property("RetentionExpiresAt"); - - b.Property("RetentionMode") - .IsRequired(); - - b.Property("Status") - .IsRequired(); - - b.Property("TenantId"); - - b.Property("Title"); - - b.Property("UpdatedAt"); - - b.Property("UserId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("RetentionExpiresAt"); - - b.HasIndex("Status"); - - b.HasIndex("TenantId", "UserId"); - - b.ToTable("AIConversations"); - }); - - modelBuilder.Entity("Elsa.AI.Persistence.EFCore.Entities.AIProposalRecord", b => - { - b.Property("Id"); - - b.Property("AppliedBy"); - - b.Property("AppliedAt"); - - b.Property("BaselineVersionId"); - - b.Property("BaselineWorkflowDefinitionId"); - - b.Property("ConversationId") - .IsRequired(); - - b.Property("CreatedAt"); - - b.Property("CreatedBy") - .IsRequired(); - - b.Property("GraphDiff"); - - b.Property("Kind") - .IsRequired(); - - b.Property("Rationale") - .IsRequired(); - - b.Property("ReviewedBy"); - - b.Property("ReviewedAt"); - - b.Property("Status") - .IsRequired(); - - b.Property("TenantId"); - - b.Property("ValidationDiagnostics") - .IsRequired(); - - b.Property("Warnings") - .IsRequired(); - - b.Property("WorkflowPayload") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("Status"); - - b.HasIndex("TenantId", "ConversationId"); - - b.ToTable("AIProposals"); - }); -#pragma warning restore 612, 618 - } -} diff --git a/src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/AIPersistenceFeature.cs b/src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/AIPersistenceFeature.cs deleted file mode 100644 index 08652dcb8..000000000 --- a/src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/AIPersistenceFeature.cs +++ /dev/null @@ -1,25 +0,0 @@ -using CShells.Features; -using Elsa.Extensions; -using Elsa.Platform.PackageManifest.Generator.Hints; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace Elsa.AI.Persistence.EFCore.ShellFeatures; - -[ManifestFeatureCategory("AI")] -[ManifestFeatureCategory("Persistence")] -[ShellFeature( - "AIPersistence", - DisplayName = "AI Persistence", - Description = "Registers durable EF Core stores for Weaver AI conversations, proposals and audit records")] -[UsedImplicitly] -public class AIPersistenceFeature : IShellFeature -{ - public Action? ConfigureDbContext { get; set; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddAIPersistenceStores(ConfigureDbContext); - } -} diff --git a/src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/EFCoreAIPersistenceShellFeatureBase.cs b/src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/EFCoreAIPersistenceShellFeatureBase.cs new file mode 100644 index 000000000..82845da2f --- /dev/null +++ b/src/modules/Elsa.AI.Persistence.EFCore/ShellFeatures/EFCoreAIPersistenceShellFeatureBase.cs @@ -0,0 +1,19 @@ +using Elsa.Extensions; +using Elsa.Persistence.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.AI.Persistence.EFCore.ShellFeatures; + +/// +/// Base class for provider-specific AI persistence shell features. +/// +public abstract class EFCoreAIPersistenceShellFeatureBase : PersistenceShellFeatureBase +{ + /// + protected override void OnConfiguring(IServiceCollection services) + { + services.AddScoped(sp => sp.GetRequiredService>().CreateDbContext()); + services.AddAIPersistenceStoreServices(); + } +} diff --git a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/AIDbContextOptionsBuilderExtensions.cs b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/AIDbContextOptionsBuilderExtensions.cs new file mode 100644 index 000000000..e2db7be89 --- /dev/null +++ b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/AIDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Elsa.AI.Persistence.EFCore.Sqlite; +using Elsa.Persistence.EFCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.AI.Persistence.EFCore.UnitTests; + +internal static class AIDbContextOptionsBuilderExtensions +{ + public static DbContextOptionsBuilder UseSqliteAIMigrations(this DbContextOptionsBuilder builder, SqliteConnection connection) + { + builder.UseElsaDbContextOptions(null); + return builder.UseSqlite(connection, db => db.MigrationsAssembly(typeof(AIDbContextFactory).Assembly.GetName().Name)); + } +} diff --git a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIAuditSinkTests.cs b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIAuditSinkTests.cs index ef5cff7c0..62c5bcf12 100644 --- a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIAuditSinkTests.cs +++ b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIAuditSinkTests.cs @@ -102,7 +102,7 @@ public class EFCoreAIAuditSinkTests : IAsyncLifetime public async Task InitializeAsync() { await _connection.OpenAsync(); - _dbContext = new AIDbContext(new DbContextOptionsBuilder().UseSqlite(_connection).Options); + _dbContext = new AIDbContext(new DbContextOptionsBuilder().UseSqliteAIMigrations(_connection).Options); await _dbContext.Database.MigrateAsync(); } diff --git a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIConversationStoreTests.cs b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIConversationStoreTests.cs index 5efb1cca5..6a42112cd 100644 --- a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIConversationStoreTests.cs +++ b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIConversationStoreTests.cs @@ -499,7 +499,7 @@ public class EFCoreAIConversationStoreTests : IAsyncLifetime public async Task InitializeAsync() { await _connection.OpenAsync(); - _dbContext = new AIDbContext(new DbContextOptionsBuilder().UseSqlite(_connection).Options); + _dbContext = new AIDbContext(new DbContextOptionsBuilder().UseSqliteAIMigrations(_connection).Options); await _dbContext.Database.MigrateAsync(); } diff --git a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIProposalStoreTests.cs b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIProposalStoreTests.cs index 3953859e3..1f0cf3797 100644 --- a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIProposalStoreTests.cs +++ b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/EFCoreAIProposalStoreTests.cs @@ -209,7 +209,7 @@ public class EFCoreAIProposalStoreTests : IAsyncLifetime public async Task ProposalStoreRetriesConcurrentInsertsAsUpdates() { var options = new DbContextOptionsBuilder() - .UseSqlite(_connection) + .UseSqliteAIMigrations(_connection) .AddInterceptors(new ConcurrentProposalInsertInterceptor(_connection)) .Options; await using var context = new AIDbContext(options); @@ -344,7 +344,7 @@ public class EFCoreAIProposalStoreTests : IAsyncLifetime public async Task InitializeAsync() { await _connection.OpenAsync(); - _dbContext = new AIDbContext(new DbContextOptionsBuilder().UseSqlite(_connection).Options); + _dbContext = new AIDbContext(new DbContextOptionsBuilder().UseSqliteAIMigrations(_connection).Options); await _dbContext.Database.MigrateAsync(); } @@ -368,7 +368,7 @@ public class EFCoreAIProposalStoreTests : IAsyncLifetime return result; _inserted = true; - var options = new DbContextOptionsBuilder().UseSqlite(connection).Options; + var options = new DbContextOptionsBuilder().UseSqliteAIMigrations(connection).Options; await using var competingContext = new AIDbContext(options); competingContext.Proposals.Add(new AIProposalRecord { diff --git a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj index 54945fab7..bc1932677 100644 --- a/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj +++ b/test/unit/Elsa.AI.Persistence.EFCore.UnitTests/Elsa.AI.Persistence.EFCore.UnitTests.csproj @@ -7,6 +7,7 @@ +