diff --git a/src/api/Data/AppDbContext.cs b/src/api/Data/AppDbContext.cs index 7055dc6..54dd506 100644 --- a/src/api/Data/AppDbContext.cs +++ b/src/api/Data/AppDbContext.cs @@ -1,3 +1,4 @@ +using Api.Models; using Microsoft.EntityFrameworkCore; namespace Api.Data; @@ -8,10 +9,176 @@ public class AppDbContext : DbContext { } + public DbSet Users => Set(); + public DbSet Workspaces => Set(); + public DbSet Projects => Set(); + public DbSet Domains => Set(); + public DbSet ShortLinks => Set(); + public DbSet QRCodeDesigns => Set(); + public DbSet Events => Set(); + public DbSet Assets => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // Entity configurations will be added here as entities are created + // User configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Email).IsUnique(); + entity.Property(e => e.Email).HasMaxLength(255); + entity.Property(e => e.PasswordHash).HasMaxLength(255); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + }); + + // Workspace configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).HasMaxLength(100); + entity.Property(e => e.Plan).HasConversion().HasMaxLength(20); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.HasOne(e => e.Owner) + .WithMany(u => u.Workspaces) + .HasForeignKey(e => e.OwnerUserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Project configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Name).HasMaxLength(100); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.HasOne(e => e.Workspace) + .WithMany(w => w.Projects) + .HasForeignKey(e => e.WorkspaceId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Domain configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Hostname).IsUnique(); + entity.Property(e => e.Hostname).HasMaxLength(255); + entity.Property(e => e.Status).HasConversion().HasMaxLength(20); + entity.Property(e => e.VerificationToken).HasMaxLength(64); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.HasOne(e => e.Workspace) + .WithMany(w => w.Domains) + .HasForeignKey(e => e.WorkspaceId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // ShortLink configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => new { e.DomainId, e.Slug }).IsUnique(); + entity.Property(e => e.Slug).HasMaxLength(50); + entity.Property(e => e.DestinationUrl).HasMaxLength(2048); + entity.Property(e => e.Title).HasMaxLength(255); + entity.Property(e => e.Status).HasConversion().HasMaxLength(20); + entity.Property(e => e.PasswordHash).HasMaxLength(255); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + entity.Property(e => e.UpdatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.HasOne(e => e.Workspace) + .WithMany(w => w.ShortLinks) + .HasForeignKey(e => e.WorkspaceId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Project) + .WithMany(p => p.ShortLinks) + .HasForeignKey(e => e.ProjectId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.Domain) + .WithMany(d => d.ShortLinks) + .HasForeignKey(e => e.DomainId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // QRCodeDesign configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.StyleJson).HasColumnType("jsonb"); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + entity.Property(e => e.UpdatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.HasOne(e => e.Workspace) + .WithMany(w => w.QRCodeDesigns) + .HasForeignKey(e => e.WorkspaceId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Project) + .WithMany(p => p.QRCodeDesigns) + .HasForeignKey(e => e.ProjectId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.ShortLink) + .WithMany(s => s.QRCodeDesigns) + .HasForeignKey(e => e.ShortLinkId) + .OnDelete(DeleteBehavior.SetNull); + + entity.HasOne(e => e.LogoAsset) + .WithMany() + .HasForeignKey(e => e.LogoAssetId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // Event configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Type).HasConversion().HasMaxLength(10); + entity.Property(e => e.Timestamp).HasDefaultValueSql("CURRENT_TIMESTAMP"); + entity.Property(e => e.IpHash).HasMaxLength(64); + entity.Property(e => e.UserAgent).HasMaxLength(512); + entity.Property(e => e.Referrer).HasMaxLength(2048); + entity.Property(e => e.CountryCode).HasMaxLength(2); + entity.Property(e => e.DeviceType).HasMaxLength(20); + entity.Property(e => e.DedupeKey).HasMaxLength(128); + + entity.HasIndex(e => e.Timestamp); + entity.HasIndex(e => new { e.ShortLinkId, e.Timestamp }); + entity.HasIndex(e => new { e.WorkspaceId, e.Timestamp }); + + entity.HasOne(e => e.Workspace) + .WithMany(w => w.Events) + .HasForeignKey(e => e.WorkspaceId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.ShortLink) + .WithMany(s => s.Events) + .HasForeignKey(e => e.ShortLinkId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.QRCode) + .WithMany(q => q.Events) + .HasForeignKey(e => e.QRCodeId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // Asset configuration + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Type).HasConversion().HasMaxLength(20); + entity.Property(e => e.StorageKey).HasMaxLength(512); + entity.Property(e => e.Mime).HasMaxLength(100); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.HasOne(e => e.Workspace) + .WithMany(w => w.Assets) + .HasForeignKey(e => e.WorkspaceId) + .OnDelete(DeleteBehavior.Cascade); + }); } } diff --git a/src/api/Migrations/20260127192536_InitialCreate.Designer.cs b/src/api/Migrations/20260127192536_InitialCreate.Designer.cs new file mode 100644 index 0000000..ad19fcf --- /dev/null +++ b/src/api/Migrations/20260127192536_InitialCreate.Designer.cs @@ -0,0 +1,203 @@ +// +using System; +using Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260127192536_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Api.Models.Domain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Hostname") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("VerificationToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Hostname") + .IsUnique(); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Domains"); + }); + + modelBuilder.Entity("Api.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Api.Models.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OwnerUserId") + .HasColumnType("uuid"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Workspaces"); + }); + + modelBuilder.Entity("Api.Models.Domain", b => + { + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("Domains") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.Project", b => + { + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("Projects") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.Workspace", b => + { + b.HasOne("Api.Models.User", "Owner") + .WithMany("Workspaces") + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Api.Models.User", b => + { + b.Navigation("Workspaces"); + }); + + modelBuilder.Entity("Api.Models.Workspace", b => + { + b.Navigation("Domains"); + + b.Navigation("Projects"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/api/Migrations/20260127192536_InitialCreate.cs b/src/api/Migrations/20260127192536_InitialCreate.cs new file mode 100644 index 0000000..0578fb6 --- /dev/null +++ b/src/api/Migrations/20260127192536_InitialCreate.cs @@ -0,0 +1,136 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace api.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Email = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + PasswordHash = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + VerifiedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Workspaces", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OwnerUserId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Plan = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_Workspaces", x => x.Id); + table.ForeignKey( + name: "FK_Workspaces_Users_OwnerUserId", + column: x => x.OwnerUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Domains", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + WorkspaceId = table.Column(type: "uuid", nullable: false), + Hostname = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + VerificationToken = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_Domains", x => x.Id); + table.ForeignKey( + name: "FK_Domains_Workspaces_WorkspaceId", + column: x => x.WorkspaceId, + principalTable: "Workspaces", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Projects", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + WorkspaceId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("PK_Projects", x => x.Id); + table.ForeignKey( + name: "FK_Projects_Workspaces_WorkspaceId", + column: x => x.WorkspaceId, + principalTable: "Workspaces", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Domains_Hostname", + table: "Domains", + column: "Hostname", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Domains_WorkspaceId", + table: "Domains", + column: "WorkspaceId"); + + migrationBuilder.CreateIndex( + name: "IX_Projects_WorkspaceId", + table: "Projects", + column: "WorkspaceId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Workspaces_OwnerUserId", + table: "Workspaces", + column: "OwnerUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Domains"); + + migrationBuilder.DropTable( + name: "Projects"); + + migrationBuilder.DropTable( + name: "Workspaces"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/src/api/Migrations/AppDbContextModelSnapshot.cs b/src/api/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..9e037a0 --- /dev/null +++ b/src/api/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,537 @@ +// +using System; +using Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace api.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Api.Models.Asset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Mime") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Assets"); + }); + + modelBuilder.Entity("Api.Models.Domain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Hostname") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("VerificationToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Hostname") + .IsUnique(); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Domains"); + }); + + modelBuilder.Entity("Api.Models.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CountryCode") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("DedupeKey") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DeviceType") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("IpHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("QRCodeId") + .HasColumnType("uuid"); + + b.Property("Referrer") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ShortLinkId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("QRCodeId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("ShortLinkId", "Timestamp"); + + b.HasIndex("WorkspaceId", "Timestamp"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Api.Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Api.Models.QRCodeDesign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LogoAssetId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ShortLinkId") + .HasColumnType("uuid"); + + b.Property("StyleJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("LogoAssetId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("ShortLinkId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("QRCodeDesigns"); + }); + + modelBuilder.Entity("Api.Models.ShortLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DestinationUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("DomainId") + .HasColumnType("uuid"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("DomainId", "Slug") + .IsUnique(); + + b.ToTable("ShortLinks"); + }); + + modelBuilder.Entity("Api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Api.Models.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OwnerUserId") + .HasColumnType("uuid"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Workspaces"); + }); + + modelBuilder.Entity("Api.Models.Asset", b => + { + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("Assets") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.Domain", b => + { + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("Domains") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.Event", b => + { + b.HasOne("Api.Models.QRCodeDesign", "QRCode") + .WithMany("Events") + .HasForeignKey("QRCodeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Api.Models.ShortLink", "ShortLink") + .WithMany("Events") + .HasForeignKey("ShortLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("Events") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("QRCode"); + + b.Navigation("ShortLink"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.Project", b => + { + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("Projects") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.QRCodeDesign", b => + { + b.HasOne("Api.Models.Asset", "LogoAsset") + .WithMany() + .HasForeignKey("LogoAssetId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Api.Models.Project", "Project") + .WithMany("QRCodeDesigns") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Api.Models.ShortLink", "ShortLink") + .WithMany("QRCodeDesigns") + .HasForeignKey("ShortLinkId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("QRCodeDesigns") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LogoAsset"); + + b.Navigation("Project"); + + b.Navigation("ShortLink"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.ShortLink", b => + { + b.HasOne("Api.Models.Domain", "Domain") + .WithMany("ShortLinks") + .HasForeignKey("DomainId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Api.Models.Project", "Project") + .WithMany("ShortLinks") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Api.Models.Workspace", "Workspace") + .WithMany("ShortLinks") + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Domain"); + + b.Navigation("Project"); + + b.Navigation("Workspace"); + }); + + modelBuilder.Entity("Api.Models.Workspace", b => + { + b.HasOne("Api.Models.User", "Owner") + .WithMany("Workspaces") + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Api.Models.Domain", b => + { + b.Navigation("ShortLinks"); + }); + + modelBuilder.Entity("Api.Models.Project", b => + { + b.Navigation("QRCodeDesigns"); + + b.Navigation("ShortLinks"); + }); + + modelBuilder.Entity("Api.Models.QRCodeDesign", b => + { + b.Navigation("Events"); + }); + + modelBuilder.Entity("Api.Models.ShortLink", b => + { + b.Navigation("Events"); + + b.Navigation("QRCodeDesigns"); + }); + + modelBuilder.Entity("Api.Models.User", b => + { + b.Navigation("Workspaces"); + }); + + modelBuilder.Entity("Api.Models.Workspace", b => + { + b.Navigation("Assets"); + + b.Navigation("Domains"); + + b.Navigation("Events"); + + b.Navigation("Projects"); + + b.Navigation("QRCodeDesigns"); + + b.Navigation("ShortLinks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/api/Models/Domain.cs b/src/api/Models/Domain.cs new file mode 100644 index 0000000..e712045 --- /dev/null +++ b/src/api/Models/Domain.cs @@ -0,0 +1,22 @@ +namespace Api.Models; + +public enum DomainStatus +{ + Pending, + Verified, + Active +} + +public class Domain +{ + public Guid Id { get; set; } + public Guid WorkspaceId { get; set; } + public required string Hostname { get; set; } + public DomainStatus Status { get; set; } = DomainStatus.Pending; + public required string VerificationToken { get; set; } + public DateTime CreatedAt { get; set; } + + // Navigation properties + public Workspace Workspace { get; set; } = null!; + public ICollection ShortLinks { get; set; } = []; +} diff --git a/src/api/Models/Project.cs b/src/api/Models/Project.cs new file mode 100644 index 0000000..9745389 --- /dev/null +++ b/src/api/Models/Project.cs @@ -0,0 +1,14 @@ +namespace Api.Models; + +public class Project +{ + public Guid Id { get; set; } + public Guid WorkspaceId { get; set; } + public required string Name { get; set; } + public DateTime CreatedAt { get; set; } + + // Navigation properties + public Workspace Workspace { get; set; } = null!; + public ICollection ShortLinks { get; set; } = []; + public ICollection QRCodeDesigns { get; set; } = []; +} diff --git a/src/api/Models/User.cs b/src/api/Models/User.cs new file mode 100644 index 0000000..ed8e236 --- /dev/null +++ b/src/api/Models/User.cs @@ -0,0 +1,13 @@ +namespace Api.Models; + +public class User +{ + public Guid Id { get; set; } + public required string Email { get; set; } + public required string PasswordHash { get; set; } + public DateTime? VerifiedAt { get; set; } + public DateTime CreatedAt { get; set; } + + // Navigation properties + public ICollection Workspaces { get; set; } = []; +} diff --git a/src/api/Models/Workspace.cs b/src/api/Models/Workspace.cs new file mode 100644 index 0000000..e7bc33d --- /dev/null +++ b/src/api/Models/Workspace.cs @@ -0,0 +1,26 @@ +namespace Api.Models; + +public enum WorkspacePlan +{ + Free, + Pro, + Business +} + +public class Workspace +{ + public Guid Id { get; set; } + public Guid OwnerUserId { get; set; } + public required string Name { get; set; } + public WorkspacePlan Plan { get; set; } = WorkspacePlan.Free; + public DateTime CreatedAt { get; set; } + + // Navigation properties + public User Owner { get; set; } = null!; + public ICollection Projects { get; set; } = []; + public ICollection Domains { get; set; } = []; + public ICollection ShortLinks { get; set; } = []; + public ICollection QRCodeDesigns { get; set; } = []; + public ICollection Events { get; set; } = []; + public ICollection Assets { get; set; } = []; +}