feat(infra): update db context with modeling for institution and relationships

This commit is contained in:
Stevan Freeborn
2026-02-12 16:19:51 -06:00
parent d80ba9b24c
commit 2c17dc0662
6 changed files with 464 additions and 4 deletions
@@ -0,0 +1,27 @@
namespace FiscalOS.Infra.Accounts.Plaid;
public sealed class PlaidMetadata : InstitutionMetadata
{
public const string TypeValue = "Plaid";
public string PlaidId { get; init; } = string.Empty;
public string PlaidName { get; init; } = string.Empty;
public string EncryptedAccessToken { get; init; } = string.Empty;
private PlaidMetadata() : base(TypeValue)
{
}
public static PlaidMetadata From(
string plaidId,
string plaidName,
string encryptedAccessToken
)
{
return new PlaidMetadata
{
PlaidId = plaidId,
PlaidName = plaidName,
EncryptedAccessToken = encryptedAccessToken
};
}
}
+37 -4
View File
@@ -49,15 +49,20 @@ public sealed class AppDbContext(
modelBuilder.Entity<User>(static eb => modelBuilder.Entity<User>(static eb =>
{ {
eb.Property(static u => u.Username);
eb.HasIndex(static u => u.Username).IsUnique();
eb.Property(static u => u.HashedPassword);
eb.HasMany(static u => u.RefreshTokens) eb.HasMany(static u => u.RefreshTokens)
.WithOne(static t => t.User) .WithOne(static t => t.User)
.HasForeignKey(static t => t.UserId) .HasForeignKey(static t => t.UserId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
eb.Property(static u => u.Username); eb.HasMany(static u => u.Institutions)
eb.HasIndex(static u => u.Username).IsUnique(); .WithOne()
.HasForeignKey(static i => i.UserId)
eb.Property(static u => u.HashedPassword); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity<RefreshToken>(static eb => modelBuilder.Entity<RefreshToken>(static eb =>
@@ -69,5 +74,33 @@ public sealed class AppDbContext(
eb.Property(static t => t.Token); eb.Property(static t => t.Token);
eb.HasIndex(static t => t.Token).IsUnique(); eb.HasIndex(static t => t.Token).IsUnique();
}); });
modelBuilder.Entity<Institution>(static eb =>
{
eb.Property(static i => i.Name);
eb.HasOne(static i => i.Metadata)
.WithOne()
.HasForeignKey<InstitutionMetadata>(static m => m.InstitutionId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<InstitutionMetadata>(static eb =>
{
eb.HasDiscriminator(static m => m.Type)
.HasValue<Accounts.Plaid.PlaidMetadata>(Accounts.Plaid.PlaidMetadata.TypeValue);
eb.Property(static m => m.InstitutionId);
eb.Property(static m => m.Type);
});
modelBuilder.Entity<Accounts.Plaid.PlaidMetadata>(static eb =>
{
eb.HasBaseType<InstitutionMetadata>();
eb.Property(static m => m.PlaidId);
eb.Property(static m => m.PlaidName);
eb.Property(static m => m.EncryptedAccessToken);
});
} }
} }
@@ -0,0 +1,215 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260212215015_InstitutionAndMetadata")]
partial class InstitutionAndMetadata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata")
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("Institutions");
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,81 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class InstitutionAndMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Institution",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Institution", x => x.Id);
table.ForeignKey(
name: "FK_Institution_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "InstitutionMetadata",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 21, nullable: false),
InstitutionId = table.Column<Guid>(type: "TEXT", nullable: false),
PlaidId = table.Column<string>(type: "TEXT", nullable: true),
PlaidName = table.Column<string>(type: "TEXT", nullable: true),
EncryptedAccessToken = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InstitutionMetadata", x => x.Id);
table.ForeignKey(
name: "FK_InstitutionMetadata_Institution_InstitutionId",
column: x => x.InstitutionId,
principalTable: "Institution",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Institution_UserId",
table: "Institution",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_InstitutionMetadata_InstitutionId",
table: "InstitutionMetadata",
column: "InstitutionId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InstitutionMetadata");
migrationBuilder.DropTable(
name: "Institution");
}
}
}
@@ -17,6 +17,64 @@ namespace FiscalOS.Infra.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b => modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -88,6 +146,43 @@ namespace FiscalOS.Infra.Migrations
b.ToTable("Users"); b.ToTable("Users");
}); });
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b => modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{ {
b.HasOne("FiscalOS.Core.Identity.User", "User") b.HasOne("FiscalOS.Core.Identity.User", "User")
@@ -99,8 +194,16 @@ namespace FiscalOS.Infra.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata")
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b => modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{ {
b.Navigation("Institutions");
b.Navigation("RefreshTokens"); b.Navigation("RefreshTokens");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
+1
View File
@@ -5,6 +5,7 @@ global using System.Security.Cryptography;
global using System.Text; global using System.Text;
global using System.Text.Json; global using System.Text.Json;
global using FiscalOS.Core.Accounts;
global using FiscalOS.Core.Authentication; global using FiscalOS.Core.Authentication;
global using FiscalOS.Core.Data; global using FiscalOS.Core.Data;
global using FiscalOS.Core.Identity; global using FiscalOS.Core.Identity;