diff --git a/.gitignore b/.gitignore index acc2bc71..a8d90291 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ obj .suo /src/Flow.sln.DotSettings.user /src/.vs +/src/.env \ No newline at end of file diff --git a/src/.config/dotnet-tools.json b/src/.config/dotnet-tools.json new file mode 100644 index 00000000..d858beba --- /dev/null +++ b/src/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "5.0.11", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/src/Flow.sln b/src/Flow.sln index f587f61a..00736caf 100644 --- a/src/Flow.sln +++ b/src/Flow.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Infrastructure.IO", "l EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Infrastructure.IO.UnitTests", "l2\Flow.Infrastructure.IO.UnitTests\Flow.Infrastructure.IO.UnitTests.csproj", "{AAB611F2-902B-4196-97B8-E48EEE41E010}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Infrastructure.Storage.Migrations", "l2\Flow.Infrastructure.Storage.Migrations\Flow.Infrastructure.Storage.Migrations.csproj", "{D4126756-6928-4D1F-B203-DC1A4E38AD2A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,10 @@ Global {AAB611F2-902B-4196-97B8-E48EEE41E010}.Debug|Any CPU.Build.0 = Debug|Any CPU {AAB611F2-902B-4196-97B8-E48EEE41E010}.Release|Any CPU.ActiveCfg = Release|Any CPU {AAB611F2-902B-4196-97B8-E48EEE41E010}.Release|Any CPU.Build.0 = Release|Any CPU + {D4126756-6928-4D1F-B203-DC1A4E38AD2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4126756-6928-4D1F-B203-DC1A4E38AD2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4126756-6928-4D1F-B203-DC1A4E38AD2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4126756-6928-4D1F-B203-DC1A4E38AD2A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -83,6 +89,7 @@ Global {25C4F714-68D3-42E6-A6CB-5E063DBCDE63} = {C2DD4A7B-704F-4C12-B89E-E6A7657A418D} {C99CF6F9-CFE3-4F67-B450-89B45AD7893F} = {C2DD4A7B-704F-4C12-B89E-E6A7657A418D} {AAB611F2-902B-4196-97B8-E48EEE41E010} = {C2DD4A7B-704F-4C12-B89E-E6A7657A418D} + {D4126756-6928-4D1F-B203-DC1A4E38AD2A} = {C2DD4A7B-704F-4C12-B89E-E6A7657A418D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BD863072-1B8D-417A-BFF4-C60D20CE1D31} diff --git a/src/docker-compose.devenv.yaml b/src/docker-compose.devenv.yaml new file mode 100644 index 00000000..b1973a40 --- /dev/null +++ b/src/docker-compose.devenv.yaml @@ -0,0 +1,16 @@ +version: "3.8" +services: + accounts-db: + image: postgres + restart: always + volumes: + - flow-storage-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=flow-dev + - "POSTGRES_USER=${DEV_DB_USER}" + - "POSTGRES_PASSWORD=${DEV_DB_PWD}" + ports: + - "${DEV_DB_PORT}:5432" + +volumes: + flow-storage-data: \ No newline at end of file diff --git a/src/l0/Flow.Domain.Transactions/AccountInfo.cs b/src/l0/Flow.Domain.Transactions/AccountInfo.cs index ee51bbdc..78c64352 100644 --- a/src/l0/Flow.Domain.Transactions/AccountInfo.cs +++ b/src/l0/Flow.Domain.Transactions/AccountInfo.cs @@ -4,6 +4,8 @@ namespace Flow.Domain.Transactions { public sealed class AccountInfo { + public static readonly AccountInfo Empty = new AccountInfo(string.Empty, string.Empty); + public AccountInfo(string name, string bank) { Name = name; diff --git a/src/l0/Flow.Domain.Transactions/RecordedTransaction.cs b/src/l0/Flow.Domain.Transactions/RecordedTransaction.cs index 8d3c2762..5e2a73f5 100644 --- a/src/l0/Flow.Domain.Transactions/RecordedTransaction.cs +++ b/src/l0/Flow.Domain.Transactions/RecordedTransaction.cs @@ -2,8 +2,13 @@ namespace Flow.Domain.Transactions { - public sealed class RecordedTransaction: Transaction + public class RecordedTransaction: Transaction { + public RecordedTransaction(long key, DateTime timestamp, decimal amount, string currency, string? category, string title) + : this(key, timestamp, amount, currency, category, title, AccountInfo.Empty) + { + } + public RecordedTransaction(long key, DateTime timestamp, decimal amount, string currency, string? category, string title, AccountInfo account) : base(timestamp, amount, currency, category, title, account) { Key = key; diff --git a/src/l2/Flow.Infrastructure.Storage.Migrations/DesignTimeDbContextBuilderFactory.cs b/src/l2/Flow.Infrastructure.Storage.Migrations/DesignTimeDbContextBuilderFactory.cs new file mode 100644 index 00000000..3578c1c8 --- /dev/null +++ b/src/l2/Flow.Infrastructure.Storage.Migrations/DesignTimeDbContextBuilderFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Flow.Infrastructure.Storage.Migrations; + +internal class DesignTimeDbContextBuilderFactory : IDesignTimeDbContextFactory +{ + public FlowDbContext CreateDbContext(string[] args) + { + var options = new DbContextOptionsBuilder() + .UseNpgsql( + args.FirstOrDefault() ?? throw new InvalidOperationException("Connection string was not configured!"), + o => o.MigrationsAssembly(typeof(DesignTimeDbContextBuilderFactory).Assembly.FullName)) + .Options; + + return new FlowDbContext(options); + } +} \ No newline at end of file diff --git a/src/l2/Flow.Infrastructure.Storage.Migrations/Flow.Infrastructure.Storage.Migrations.csproj b/src/l2/Flow.Infrastructure.Storage.Migrations/Flow.Infrastructure.Storage.Migrations.csproj new file mode 100644 index 00000000..6aeb447e --- /dev/null +++ b/src/l2/Flow.Infrastructure.Storage.Migrations/Flow.Infrastructure.Storage.Migrations.csproj @@ -0,0 +1,21 @@ + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + Exe + net6.0 + enable + enable + + + diff --git a/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/20211106231710_initial.Designer.cs b/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/20211106231710_initial.Designer.cs new file mode 100644 index 00000000..b2059e9d --- /dev/null +++ b/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/20211106231710_initial.Designer.cs @@ -0,0 +1,114 @@ +// +using System; +using Flow.Infrastructure.Storage; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Flow.Infrastructure.Storage.Migrations.Migrations +{ + [DbContext(typeof(FlowDbContext))] + [Migration("20211106231710_initial")] + partial class initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Flow.Domain.Transactions.AccountInfo", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("Bank") + .HasColumnType("text"); + + b.HasKey("Name", "Bank"); + + b.ToTable("AccountInfo"); + }); + + modelBuilder.Entity("Flow.Domain.Transactions.RecordedTransaction", b => + { + b.Property("Key") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("Category") + .IsRequired() + .HasColumnType("text"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("account_name") + .IsRequired() + .HasColumnType("text"); + + b.Property("bank_name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Key"); + + b.HasIndex("account_name", "bank_name"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("Flow.Domain.Transactions.RecordedTransaction", b => + { + b.HasOne("Flow.Domain.Transactions.AccountInfo", null) + .WithMany() + .HasForeignKey("account_name", "bank_name") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Flow.Domain.Transactions.Overrides", "Overrides", b1 => + { + b1.Property("RecordedTransactionKey") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b1.Property("Category") + .HasColumnType("text"); + + b1.Property("Comment") + .HasColumnType("text"); + + b1.Property("Title") + .HasColumnType("text"); + + b1.HasKey("RecordedTransactionKey"); + + b1.ToTable("Transactions"); + + b1.WithOwner() + .HasForeignKey("RecordedTransactionKey"); + }); + + b.Navigation("Overrides"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/20211106231710_initial.cs b/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/20211106231710_initial.cs new file mode 100644 index 00000000..31cd3b43 --- /dev/null +++ b/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/20211106231710_initial.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Flow.Infrastructure.Storage.Migrations.Migrations +{ + public partial class initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AccountInfo", + columns: table => new + { + Name = table.Column(type: "text", nullable: false), + Bank = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccountInfo", x => new { x.Name, x.Bank }); + }); + + migrationBuilder.CreateTable( + name: "Transactions", + columns: table => new + { + Key = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Overrides_Comment = table.Column(type: "text", nullable: true), + Overrides_Title = table.Column(type: "text", nullable: true), + Overrides_Category = table.Column(type: "text", nullable: true), + account_name = table.Column(type: "text", nullable: false), + bank_name = table.Column(type: "text", nullable: false), + Timestamp = table.Column(type: "timestamp without time zone", nullable: false), + Amount = table.Column(type: "numeric", nullable: false), + Currency = table.Column(type: "text", nullable: false), + Category = table.Column(type: "text", nullable: false), + Title = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Transactions", x => x.Key); + table.ForeignKey( + name: "FK_Transactions_AccountInfo_account_name_bank_name", + columns: x => new { x.account_name, x.bank_name }, + principalTable: "AccountInfo", + principalColumns: new[] { "Name", "Bank" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Transactions_account_name_bank_name", + table: "Transactions", + columns: new[] { "account_name", "bank_name" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Transactions"); + + migrationBuilder.DropTable( + name: "AccountInfo"); + } + } +} diff --git a/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/FlowDbContextModelSnapshot.cs b/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/FlowDbContextModelSnapshot.cs new file mode 100644 index 00000000..0277b775 --- /dev/null +++ b/src/l2/Flow.Infrastructure.Storage.Migrations/Migrations/FlowDbContextModelSnapshot.cs @@ -0,0 +1,112 @@ +// +using System; +using Flow.Infrastructure.Storage; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Flow.Infrastructure.Storage.Migrations.Migrations +{ + [DbContext(typeof(FlowDbContext))] + partial class FlowDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Flow.Domain.Transactions.AccountInfo", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("Bank") + .HasColumnType("text"); + + b.HasKey("Name", "Bank"); + + b.ToTable("AccountInfo"); + }); + + modelBuilder.Entity("Flow.Domain.Transactions.RecordedTransaction", b => + { + b.Property("Key") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("Category") + .IsRequired() + .HasColumnType("text"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("account_name") + .IsRequired() + .HasColumnType("text"); + + b.Property("bank_name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Key"); + + b.HasIndex("account_name", "bank_name"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("Flow.Domain.Transactions.RecordedTransaction", b => + { + b.HasOne("Flow.Domain.Transactions.AccountInfo", null) + .WithMany() + .HasForeignKey("account_name", "bank_name") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Flow.Domain.Transactions.Overrides", "Overrides", b1 => + { + b1.Property("RecordedTransactionKey") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b1.Property("Category") + .HasColumnType("text"); + + b1.Property("Comment") + .HasColumnType("text"); + + b1.Property("Title") + .HasColumnType("text"); + + b1.HasKey("RecordedTransactionKey"); + + b1.ToTable("Transactions"); + + b1.WithOwner() + .HasForeignKey("RecordedTransactionKey"); + }); + + b.Navigation("Overrides"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/l2/Flow.Infrastructure.Storage.Migrations/Program.cs b/src/l2/Flow.Infrastructure.Storage.Migrations/Program.cs new file mode 100644 index 00000000..61d4734e --- /dev/null +++ b/src/l2/Flow.Infrastructure.Storage.Migrations/Program.cs @@ -0,0 +1 @@ +return 0; diff --git a/src/l2/Flow.Infrastructure.Storage/FlowDatabase.cs b/src/l2/Flow.Infrastructure.Storage/FlowDatabase.cs index afb5b902..9ecdc7ff 100644 --- a/src/l2/Flow.Infrastructure.Storage/FlowDatabase.cs +++ b/src/l2/Flow.Infrastructure.Storage/FlowDatabase.cs @@ -1,7 +1,10 @@ -using Autofac; +using System.Runtime.CompilerServices; +using Autofac; using Flow.Infrastructure.Configuration.Contract; using Microsoft.EntityFrameworkCore; +[assembly:InternalsVisibleTo("Flow.Infrastructure.Storage.Migrations")] + namespace Flow.Infrastructure.Storage; public class FlowDatabase : Module diff --git a/src/l2/Flow.Infrastructure.Storage/FlowDbContext.cs b/src/l2/Flow.Infrastructure.Storage/FlowDbContext.cs index 39eab64b..d342e1af 100644 --- a/src/l2/Flow.Infrastructure.Storage/FlowDbContext.cs +++ b/src/l2/Flow.Infrastructure.Storage/FlowDbContext.cs @@ -3,7 +3,7 @@ namespace Flow.Infrastructure.Storage; -internal class FlowDbContext : DbContext +internal partial class FlowDbContext : DbContext { public FlowDbContext(DbContextOptions options) : base(options) { @@ -18,6 +18,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ab.Property(a => a.Name).IsRequired(); ab.Property(a => a.Bank).IsRequired(); ab.HasKey(a => new { a.Name, a.Bank }); + ab.HasMany(typeof(RecordedTransaction)) + .WithOne() + .HasForeignKey("account_name", "bank_name"); }); modelBuilder.Entity(tb => @@ -30,7 +33,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) tb.Property(t => t.Title).IsRequired(); tb.HasKey(t => t.Key); - tb.HasOne(t => t.Account); + tb.Property(typeof(string), "account_name").IsRequired(); + tb.Property(typeof(string), "bank_name").IsRequired(); + tb.OwnsOne(t => t.Overrides, ob => { ob.Property(o => o!.Title);