diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..337014b2 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "5.0.10", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/Emulsion.Database/Emulsion.Database.fsproj b/Emulsion.Database/Emulsion.Database.fsproj new file mode 100644 index 00000000..55f084a5 --- /dev/null +++ b/Emulsion.Database/Emulsion.Database.fsproj @@ -0,0 +1,23 @@ + + + + net5.0 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Emulsion.Database/EmulsionDbContext.fs b/Emulsion.Database/EmulsionDbContext.fs new file mode 100644 index 00000000..8d26a1f3 --- /dev/null +++ b/Emulsion.Database/EmulsionDbContext.fs @@ -0,0 +1,20 @@ +namespace Emulsion.Database + +open Emulsion.Database.Models +open Microsoft.EntityFrameworkCore +open Microsoft.EntityFrameworkCore.Design + +type EmulsionDbContext(dataSource: string) = + inherit DbContext() + + [] val mutable telegramContents: DbSet + member this.TelegramContents with get() = this.telegramContents and set v = this.telegramContents <- v + + override _.OnConfiguring options = + options.UseSqlite($"Data Source={dataSource};") |> ignore + +/// This type is used by the EFCore infrastructure when creating a new migration. +type EmulsionDbContextDesignFactory() = + interface IDesignTimeDbContextFactory with + member this.CreateDbContext _ = + new EmulsionDbContext(":memory:") diff --git a/Emulsion.Database/Initializer.fs b/Emulsion.Database/Initializer.fs new file mode 100644 index 00000000..cfa1bb2e --- /dev/null +++ b/Emulsion.Database/Initializer.fs @@ -0,0 +1,7 @@ +module Emulsion.Database.Initializer + +open Microsoft.EntityFrameworkCore + +let initializeDatabase(context: EmulsionDbContext): Async = async { + do! Async.AwaitTask(context.Database.MigrateAsync()) +} diff --git a/Emulsion.Database/Migrations/20210926114410_Initialize.fs b/Emulsion.Database/Migrations/20210926114410_Initialize.fs new file mode 100644 index 00000000..14d1dbfd --- /dev/null +++ b/Emulsion.Database/Migrations/20210926114410_Initialize.fs @@ -0,0 +1,58 @@ +// +namespace Emulsion.Database.Migrations + +open System +open Emulsion.Database +open Microsoft.EntityFrameworkCore +open Microsoft.EntityFrameworkCore.Infrastructure +open Microsoft.EntityFrameworkCore.Metadata +open Microsoft.EntityFrameworkCore.Migrations +open Microsoft.EntityFrameworkCore.Storage.ValueConversion + +[)>] +[] +type Initialize() = + inherit Migration() + + override this.Up(migrationBuilder:MigrationBuilder) = + migrationBuilder.CreateTable( + name = "TelegramContents" + ,columns = (fun table -> + {| + Id = + table.Column( + nullable = false + ,``type`` = "TEXT" + ) + |}) + ,constraints = + (fun table -> + table.PrimaryKey("PK_TelegramContents", (fun x -> (x.Id) :> obj)) |> ignore + ) + ) |> ignore + + + override this.Down(migrationBuilder:MigrationBuilder) = + migrationBuilder.DropTable( + name = "TelegramContents" + ) |> ignore + + + override this.BuildTargetModel(modelBuilder: ModelBuilder) = + modelBuilder + .HasAnnotation("ProductVersion", "5.0.10") + |> ignore + + modelBuilder.Entity("Emulsion.Database.Models.TelegramContent", (fun b -> + + b.Property("Id") + .IsRequired(true) + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") |> ignore + + b.HasKey("Id") |> ignore + + b.ToTable("TelegramContents") |> ignore + + )) |> ignore + diff --git a/Emulsion.Database/Migrations/EmulsionDbContextModelSnapshot.fs b/Emulsion.Database/Migrations/EmulsionDbContextModelSnapshot.fs new file mode 100644 index 00000000..1262c8cd --- /dev/null +++ b/Emulsion.Database/Migrations/EmulsionDbContextModelSnapshot.fs @@ -0,0 +1,33 @@ +// +namespace Emulsion.Database.Migrations + +open System +open Emulsion.Database +open Microsoft.EntityFrameworkCore +open Microsoft.EntityFrameworkCore.Infrastructure +open Microsoft.EntityFrameworkCore.Metadata +open Microsoft.EntityFrameworkCore.Migrations +open Microsoft.EntityFrameworkCore.Storage.ValueConversion + +[)>] +type EmulsionDbContextModelSnapshot() = + inherit ModelSnapshot() + + override this.BuildModel(modelBuilder: ModelBuilder) = + modelBuilder + .HasAnnotation("ProductVersion", "5.0.10") + |> ignore + + modelBuilder.Entity("Emulsion.Database.Models.TelegramContent", (fun b -> + + b.Property("Id") + .IsRequired(true) + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") |> ignore + + b.HasKey("Id") |> ignore + + b.ToTable("TelegramContents") |> ignore + + )) |> ignore + diff --git a/Emulsion.Database/Models.fs b/Emulsion.Database/Models.fs new file mode 100644 index 00000000..286b068a --- /dev/null +++ b/Emulsion.Database/Models.fs @@ -0,0 +1,9 @@ +namespace Emulsion.Database.Models + +open System +open System.ComponentModel.DataAnnotations + +[] +type TelegramContent = { + [] Id: Guid +} diff --git a/Emulsion.Tests/Database/InitializerTests.fs b/Emulsion.Tests/Database/InitializerTests.fs new file mode 100644 index 00000000..eef823e9 --- /dev/null +++ b/Emulsion.Tests/Database/InitializerTests.fs @@ -0,0 +1,14 @@ +module Emulsion.Tests.Database.InitializerTests + +open System.IO +open Emulsion.Database +open Xunit + +[] +let ``Database initialization``(): unit = + async { + let databasePath = Path.Combine(Path.GetTempPath(), "emulsion-test.db") + use context = new EmulsionDbContext(databasePath) + let! _ = Async.AwaitTask(context.Database.EnsureDeletedAsync()) + do! Initializer.initializeDatabase context + } |> Async.RunSynchronously diff --git a/Emulsion.Tests/Emulsion.Tests.fsproj b/Emulsion.Tests/Emulsion.Tests.fsproj index 039a2cc2..5cf2ff3a 100644 --- a/Emulsion.Tests/Emulsion.Tests.fsproj +++ b/Emulsion.Tests/Emulsion.Tests.fsproj @@ -26,6 +26,7 @@ + @@ -37,5 +38,6 @@ + \ No newline at end of file diff --git a/Emulsion.sln b/Emulsion.sln index 66f6e503..d5da6263 100644 --- a/Emulsion.sln +++ b/Emulsion.sln @@ -26,6 +26,18 @@ ProjectSection(SolutionItems) = preProject .github\workflows\docker.yml = .github\workflows\docker.yml EndProjectSection EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Emulsion.Database", "Emulsion.Database\Emulsion.Database.fsproj", "{0111F688-1AE2-4B5D-BF46-D60B64078788}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".config", ".config", "{D03C4F4A-9806-46AA-8654-14363DFB3FBE}" +ProjectSection(SolutionItems) = preProject + .config\dotnet-tools.json = .config\dotnet-tools.json +EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{12336258-82C7-4C09-A73F-8D0B146578B2}" +ProjectSection(SolutionItems) = preProject + docs\create-migration.md = docs\create-migration.md +EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +75,18 @@ Global {1B3B65DD-FD58-45FA-B6FF-8532B513A7D9}.Release|x64.Build.0 = Release|x64 {1B3B65DD-FD58-45FA-B6FF-8532B513A7D9}.Release|x86.ActiveCfg = Release|x86 {1B3B65DD-FD58-45FA-B6FF-8532B513A7D9}.Release|x86.Build.0 = Release|x86 + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x64.ActiveCfg = Debug|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x64.Build.0 = Debug|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x86.ActiveCfg = Debug|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x86.Build.0 = Debug|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|Any CPU.Build.0 = Release|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x64.ActiveCfg = Release|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x64.Build.0 = Release|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x86.ActiveCfg = Release|Any CPU + {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {7D1ADF47-BF1C-4007-BB9B-08C283044467} = {973131E1-E645-4A50-A0D2-1886A1A8F0C6} diff --git a/README.md b/README.md index 924e9711..36a72f69 100644 --- a/README.md +++ b/README.md @@ -94,15 +94,22 @@ where `$EMULSION_VERSION` is the version of the image to publish. Documentation ------------- -- [Changelog][changelog] -- [License (MIT)][license] +Common documentation: + +- [Changelog][docs.changelog] +- [License (MIT)][docs.license] + +Developer documentation: + +- [How to Create a Database Migration][docs.create-migration] [andivionian-status-classifier]: https://github.com/ForNeVeR/andivionian-status-classifier#status-aquana- -[changelog]: ./CHANGELOG.md [docker-hub]: https://hub.docker.com/r/codingteam/emulsion +[docs.changelog]: ./CHANGELOG.md +[docs.create-migration]: ./docs/create-migration.md +[docs.license]: ./LICENSE.md [dotnet-runtime]: https://www.microsoft.com/net/download/core#/runtime [dotnet-sdk]: https://www.microsoft.com/net/download/core -[license]: ./LICENSE.md [telegram]: https://telegram.org/ [xmpp]: https://xmpp.org/ diff --git a/docs/create-migration.md b/docs/create-migration.md new file mode 100644 index 00000000..e689820f --- /dev/null +++ b/docs/create-migration.md @@ -0,0 +1,15 @@ +How to Create a Database Migration +================================== + +This article explains how to create a database migration using [EFCore.FSharp][efcore.fsharp]. + +1. Change the entity type (see `Emulsion.Database/Models.fs`), update the `EmulsionDbContext` if required. +2. Run the following shell commands: + + ```console + $ dotnet tool restore + $ cd Emulsion.Database + $ dotnet ef migrations add + ``` + +[efcore.fsharp]: https://github.com/efcore/EFCore.FSharp