feat(career): structured career profile foundation
Phase 3, schema layer. Relational children of CareerProfile — the editable master career profile. See docs/architecture/career-profile-model.md. - New entities (Models/CareerEntities.cs): CareerExperience, CareerEducation, CareerSkill, CareerProject, CareerCertification, CareerLanguage. Each carries OwnerUserId (tenant filter), a stable ItemKey (carried from the blob so future CV variants can reference items), and SortOrder. List fields persist as JSON string columns via [NotMapped] accessors — plain TEXT, reconciler-friendly. - CareerProfile gains typed child collections + a LongTailJson column (contact, summary, interests, achievements, orgs, pubs, courses, custom sections, metadata). ProfileJson becomes a derived projection for legacy read paths. - DbContext: DbSets + tenant query filters + ordered indexes; FK/cascade by convention via the typed collections. - Migration hand-edited to add only the 6 new tables + LongTailJson; the scaffolder re-emitted four reconciler-owned tables (AiWorkspaceNotes, CareerProfiles, InterviewPrepNotes, CareerProfileVersions) which were stripped. The regenerated snapshot now includes them, closing the drift. Verified against a copy of the real dev DB: applies cleanly, no data loss. Long tail (achievements/orgs/pubs/courses) starts as JSON; promotable to relational later without a source-of-truth change. Source-of-truth flip stays deferred; the blob is kept as a derived projection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,12 @@ namespace JobTrackerApi.Data
|
||||
public DbSet<UserSession> UserSessions => Set<UserSession>();
|
||||
public DbSet<CareerProfile> CareerProfiles => Set<CareerProfile>();
|
||||
public DbSet<CareerProfileVersion> CareerProfileVersions => Set<CareerProfileVersion>();
|
||||
public DbSet<CareerExperience> CareerExperiences => Set<CareerExperience>();
|
||||
public DbSet<CareerEducation> CareerEducations => Set<CareerEducation>();
|
||||
public DbSet<CareerSkill> CareerSkills => Set<CareerSkill>();
|
||||
public DbSet<CareerProject> CareerProjects => Set<CareerProject>();
|
||||
public DbSet<CareerCertification> CareerCertifications => Set<CareerCertification>();
|
||||
public DbSet<CareerLanguage> CareerLanguages => Set<CareerLanguage>();
|
||||
public DbSet<InterviewPrepNote> InterviewPrepNotes => Set<InterviewPrepNote>();
|
||||
public DbSet<AiWorkspaceNote> AiWorkspaceNotes => Set<AiWorkspaceNote>();
|
||||
|
||||
@@ -242,6 +248,17 @@ namespace JobTrackerApi.Data
|
||||
.HasForeignKey(x => x.CareerProfileId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Phase 3: relational children of CareerProfile (the editable master profile).
|
||||
// docs/architecture/career-profile-model.md. Same deny-on-null tenant filter as everything
|
||||
// else; cascade-delete with the parent; indexed by (OwnerUserId, CareerProfileId, SortOrder)
|
||||
// for the ordered per-profile reads the /career editor does.
|
||||
ConfigureCareerChild<CareerExperience>(modelBuilder);
|
||||
ConfigureCareerChild<CareerEducation>(modelBuilder);
|
||||
ConfigureCareerChild<CareerSkill>(modelBuilder);
|
||||
ConfigureCareerChild<CareerProject>(modelBuilder);
|
||||
ConfigureCareerChild<CareerCertification>(modelBuilder);
|
||||
ConfigureCareerChild<CareerLanguage>(modelBuilder);
|
||||
|
||||
// Interview prep persistence (career-workspace-implementation-roadmap.md Phase F5).
|
||||
modelBuilder.Entity<InterviewPrepNote>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
@@ -272,5 +289,17 @@ namespace JobTrackerApi.Data
|
||||
.HasForeignKey(x => x.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
// Common config for CareerProfile's relational children. The 1:many FK + cascade delete is
|
||||
// wired by convention (each child's CareerProfileId + CareerProfile nav match the typed
|
||||
// collection on CareerProfile); this adds the tenant query filter and the ordered read index.
|
||||
private void ConfigureCareerChild<T>(ModelBuilder modelBuilder) where T : Models.CareerChildEntity
|
||||
{
|
||||
modelBuilder.Entity<T>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<T>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.CareerProfileId, x.SortOrder });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1960
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobTrackerApi.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Phase 3: relational children of CareerProfile (Experience/Education/Skill/Project/
|
||||
/// Certification/Language) + the LongTailJson column on CareerProfiles. See
|
||||
/// docs/architecture/career-profile-model.md.
|
||||
///
|
||||
/// HAND-EDITED after scaffolding. `dotnet ef migrations add` also re-emitted CreateTable for
|
||||
/// AiWorkspaceNotes / CareerProfiles / InterviewPrepNotes / CareerProfileVersions — all of which
|
||||
/// already exist in every real database (provisioned by the F1/Phase-0 reconciler in
|
||||
/// StartupInitializationExtensions, which the prior ModelSnapshot did not know). Those creates
|
||||
/// were removed so the migration applies cleanly on existing databases; the regenerated
|
||||
/// snapshot now includes them, closing the drift. Only the six genuinely-new child tables and
|
||||
/// the new LongTailJson column remain here.
|
||||
/// </summary>
|
||||
public partial class AddCareerProfileRelationalChildren : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// New editable long-tail JSON on the existing CareerProfiles table.
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LongTailJson",
|
||||
table: "CareerProfiles",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CareerCertifications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Issuer = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Location = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Date = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DateNormalized = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DetailsJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CareerProfileId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ItemKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CareerCertifications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CareerCertifications_CareerProfiles_CareerProfileId",
|
||||
column: x => x.CareerProfileId,
|
||||
principalTable: "CareerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CareerEducations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Qualification = table.Column<string>(type: "TEXT", nullable: true),
|
||||
QualificationLevel = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Institution = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Location = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Start = table.Column<string>(type: "TEXT", nullable: true),
|
||||
End = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StartDate = table.Column<string>(type: "TEXT", nullable: true),
|
||||
EndDate = table.Column<string>(type: "TEXT", nullable: true),
|
||||
DetailsJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CareerProfileId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ItemKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CareerEducations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CareerEducations_CareerProfiles_CareerProfileId",
|
||||
column: x => x.CareerProfileId,
|
||||
principalTable: "CareerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CareerExperiences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Company = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Location = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Start = table.Column<string>(type: "TEXT", nullable: true),
|
||||
End = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StartDate = table.Column<string>(type: "TEXT", nullable: true),
|
||||
EndDate = table.Column<string>(type: "TEXT", nullable: true),
|
||||
IsCurrent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
BulletsJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SkillsJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CareerProfileId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ItemKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CareerExperiences", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CareerExperiences_CareerProfiles_CareerProfileId",
|
||||
column: x => x.CareerProfileId,
|
||||
principalTable: "CareerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CareerLanguages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Level = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Notes = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CareerProfileId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ItemKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CareerLanguages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CareerLanguages_CareerProfiles_CareerProfileId",
|
||||
column: x => x.CareerProfileId,
|
||||
principalTable: "CareerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CareerProjects",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Role = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Location = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Start = table.Column<string>(type: "TEXT", nullable: true),
|
||||
End = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StartDate = table.Column<string>(type: "TEXT", nullable: true),
|
||||
EndDate = table.Column<string>(type: "TEXT", nullable: true),
|
||||
BulletsJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SkillsJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LinksJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CareerProfileId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ItemKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CareerProjects", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CareerProjects_CareerProfiles_CareerProfileId",
|
||||
column: x => x.CareerProfileId,
|
||||
principalTable: "CareerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CareerSkills",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Category = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Proficiency = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CareerProfileId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ItemKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CareerSkills", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CareerSkills_CareerProfiles_CareerProfileId",
|
||||
column: x => x.CareerProfileId,
|
||||
principalTable: "CareerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerCertifications_CareerProfileId",
|
||||
table: "CareerCertifications",
|
||||
column: "CareerProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerCertifications_OwnerUserId_CareerProfileId_SortOrder",
|
||||
table: "CareerCertifications",
|
||||
columns: new[] { "OwnerUserId", "CareerProfileId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerEducations_CareerProfileId",
|
||||
table: "CareerEducations",
|
||||
column: "CareerProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerEducations_OwnerUserId_CareerProfileId_SortOrder",
|
||||
table: "CareerEducations",
|
||||
columns: new[] { "OwnerUserId", "CareerProfileId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerExperiences_CareerProfileId",
|
||||
table: "CareerExperiences",
|
||||
column: "CareerProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerExperiences_OwnerUserId_CareerProfileId_SortOrder",
|
||||
table: "CareerExperiences",
|
||||
columns: new[] { "OwnerUserId", "CareerProfileId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerLanguages_CareerProfileId",
|
||||
table: "CareerLanguages",
|
||||
column: "CareerProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder",
|
||||
table: "CareerLanguages",
|
||||
columns: new[] { "OwnerUserId", "CareerProfileId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerProjects_CareerProfileId",
|
||||
table: "CareerProjects",
|
||||
column: "CareerProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerProjects_OwnerUserId_CareerProfileId_SortOrder",
|
||||
table: "CareerProjects",
|
||||
columns: new[] { "OwnerUserId", "CareerProfileId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerSkills_CareerProfileId",
|
||||
table: "CareerSkills",
|
||||
column: "CareerProfileId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CareerSkills_OwnerUserId_CareerProfileId_SortOrder",
|
||||
table: "CareerSkills",
|
||||
columns: new[] { "OwnerUserId", "CareerProfileId", "SortOrder" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "CareerCertifications");
|
||||
migrationBuilder.DropTable(name: "CareerEducations");
|
||||
migrationBuilder.DropTable(name: "CareerExperiences");
|
||||
migrationBuilder.DropTable(name: "CareerLanguages");
|
||||
migrationBuilder.DropTable(name: "CareerProjects");
|
||||
migrationBuilder.DropTable(name: "CareerSkills");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LongTailJson",
|
||||
table: "CareerProfiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,44 @@ namespace JobTrackerApi.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.14");
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.AiWorkspaceNote", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AttachmentContextSignature")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("GeneratedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("JobApplicationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("NoteType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ResultJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobApplicationId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "JobApplicationId", "NoteType")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AiWorkspaceNotes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@@ -175,6 +213,381 @@ namespace JobTrackerApi.Migrations
|
||||
b.ToTable("Attachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerCertification", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CareerProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DateNormalized")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DetailsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Issuer")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CareerProfileId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "CareerProfileId", "SortOrder");
|
||||
|
||||
b.ToTable("CareerCertifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerEducation", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CareerProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DetailsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("End")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Institution")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Qualification")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("QualificationLevel")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Start")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CareerProfileId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "CareerProfileId", "SortOrder");
|
||||
|
||||
b.ToTable("CareerEducations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerExperience", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BulletsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CareerProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Company")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("End")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsCurrent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ItemKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SkillsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Start")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CareerProfileId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "CareerProfileId", "SortOrder");
|
||||
|
||||
b.ToTable("CareerExperiences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerLanguage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CareerProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ItemKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Level")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CareerProfileId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "CareerProfileId", "SortOrder");
|
||||
|
||||
b.ToTable("CareerLanguages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LongTailJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProfileJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CareerProfiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerProfileVersion", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CareerProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProfileJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CareerProfileId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "CareerProfileId", "Version");
|
||||
|
||||
b.ToTable("CareerProfileVersions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerProject", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BulletsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CareerProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("End")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LinksJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SkillsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Start")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CareerProfileId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "CareerProfileId", "SortOrder");
|
||||
|
||||
b.ToTable("CareerProjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerSkill", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CareerProfileId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Proficiency")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CareerProfileId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "CareerProfileId", "SortOrder");
|
||||
|
||||
b.ToTable("CareerSkills");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.Company", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -533,6 +946,52 @@ namespace JobTrackerApi.Migrations
|
||||
b.ToTable("ImapConnections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.InterviewPrepNote", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AttachmentContextSignature")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("GeneratedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("JobApplicationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LikelyQuestionsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TalkingPointsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("WeakSpotsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobApplicationId");
|
||||
|
||||
b.HasIndex("OwnerUserId", "JobApplicationId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("InterviewPrepNotes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.Job", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1224,6 +1683,17 @@ namespace JobTrackerApi.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.AiWorkspaceNote", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.JobApplication", "JobApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("JobApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("JobApplication");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.Attachment", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.JobApplication", "JobApplication")
|
||||
@@ -1235,6 +1705,83 @@ namespace JobTrackerApi.Migrations
|
||||
b.Navigation("JobApplication");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerCertification", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.CareerProfile", "CareerProfile")
|
||||
.WithMany("Certifications")
|
||||
.HasForeignKey("CareerProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CareerProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerEducation", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.CareerProfile", "CareerProfile")
|
||||
.WithMany("Education")
|
||||
.HasForeignKey("CareerProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CareerProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerExperience", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.CareerProfile", "CareerProfile")
|
||||
.WithMany("Experiences")
|
||||
.HasForeignKey("CareerProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CareerProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerLanguage", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.CareerProfile", "CareerProfile")
|
||||
.WithMany("Languages")
|
||||
.HasForeignKey("CareerProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CareerProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerProfileVersion", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.CareerProfile", "CareerProfile")
|
||||
.WithMany()
|
||||
.HasForeignKey("CareerProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CareerProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerProject", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.CareerProfile", "CareerProfile")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("CareerProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CareerProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerSkill", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.CareerProfile", "CareerProfile")
|
||||
.WithMany("Skills")
|
||||
.HasForeignKey("CareerProfileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CareerProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.Correspondence", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.JobApplication", "JobApplication")
|
||||
@@ -1256,6 +1803,17 @@ namespace JobTrackerApi.Migrations
|
||||
b.Navigation("Artifact");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.InterviewPrepNote", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.JobApplication", "JobApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("JobApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("JobApplication");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.Job", b =>
|
||||
{
|
||||
b.HasOne("JobTrackerApi.Models.Company", "Company")
|
||||
@@ -1358,6 +1916,21 @@ namespace JobTrackerApi.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.CareerProfile", b =>
|
||||
{
|
||||
b.Navigation("Certifications");
|
||||
|
||||
b.Navigation("Education");
|
||||
|
||||
b.Navigation("Experiences");
|
||||
|
||||
b.Navigation("Languages");
|
||||
|
||||
b.Navigation("Projects");
|
||||
|
||||
b.Navigation("Skills");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobTrackerApi.Models.Company", b =>
|
||||
{
|
||||
b.Navigation("Jobs");
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace JobTrackerApi.Models;
|
||||
|
||||
// Phase 3: relational children of CareerProfile — the editable master career profile.
|
||||
// See docs/architecture/career-profile-model.md.
|
||||
//
|
||||
// Design notes shared by all child entities:
|
||||
// - OwnerUserId is denormalized so the tenant global query filter applies directly (same pattern
|
||||
// as every other owned entity).
|
||||
// - ItemKey carries the stable item id from the StructuredCvProfile blob, so a row keeps its
|
||||
// identity across imports/edits and future CV variants can reference "this experience".
|
||||
// - SortOrder makes ordering explicit (the blob relied on array position).
|
||||
// - List fields persist as JSON string columns (plain TEXT — reconciler-friendly on both SQLite
|
||||
// and MySQL) exposed via [NotMapped] accessors. Children are replaced wholesale on save, so
|
||||
// fine-grained change tracking of the lists is not needed.
|
||||
|
||||
/// <summary>Base fields every CareerProfile child shares.</summary>
|
||||
public abstract class CareerChildEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CareerProfileId { get; set; }
|
||||
public CareerProfile? CareerProfile { get; set; }
|
||||
public string OwnerUserId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Stable identity carried from StructuredCvProfile so references survive edits.</summary>
|
||||
public string ItemKey { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
internal static class CareerJson
|
||||
{
|
||||
public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public static List<string> ReadList(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return new();
|
||||
try { return JsonSerializer.Deserialize<List<string>>(json, Options) ?? new(); }
|
||||
catch { return new(); }
|
||||
}
|
||||
|
||||
public static string WriteList(List<string> items) => JsonSerializer.Serialize(items ?? new(), Options);
|
||||
}
|
||||
|
||||
public sealed class CareerExperience : CareerChildEntity
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Company { get; set; }
|
||||
public string? Location { get; set; }
|
||||
// Free-text period kept alongside best-effort "YYYY-MM" normalization; neither replaces the other.
|
||||
public string? Start { get; set; }
|
||||
public string? End { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
public bool IsCurrent { get; set; }
|
||||
|
||||
public string BulletsJson { get; set; } = "[]";
|
||||
public string SkillsJson { get; set; } = "[]";
|
||||
|
||||
[NotMapped]
|
||||
public List<string> Bullets
|
||||
{
|
||||
get => CareerJson.ReadList(BulletsJson);
|
||||
set => BulletsJson = CareerJson.WriteList(value);
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public List<string> Skills
|
||||
{
|
||||
get => CareerJson.ReadList(SkillsJson);
|
||||
set => SkillsJson = CareerJson.WriteList(value);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CareerEducation : CareerChildEntity
|
||||
{
|
||||
public string? Qualification { get; set; }
|
||||
public string? QualificationLevel { get; set; }
|
||||
public string? Institution { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Start { get; set; }
|
||||
public string? End { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
|
||||
public string DetailsJson { get; set; } = "[]";
|
||||
|
||||
[NotMapped]
|
||||
public List<string> Details
|
||||
{
|
||||
get => CareerJson.ReadList(DetailsJson);
|
||||
set => DetailsJson = CareerJson.WriteList(value);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CareerSkill : CareerChildEntity
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Category { get; set; }
|
||||
public string? Proficiency { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CareerProject : CareerChildEntity
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Role { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Start { get; set; }
|
||||
public string? End { get; set; }
|
||||
public string? StartDate { get; set; }
|
||||
public string? EndDate { get; set; }
|
||||
|
||||
public string BulletsJson { get; set; } = "[]";
|
||||
public string SkillsJson { get; set; } = "[]";
|
||||
public string LinksJson { get; set; } = "[]";
|
||||
|
||||
[NotMapped]
|
||||
public List<string> Bullets
|
||||
{
|
||||
get => CareerJson.ReadList(BulletsJson);
|
||||
set => BulletsJson = CareerJson.WriteList(value);
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public List<string> Skills
|
||||
{
|
||||
get => CareerJson.ReadList(SkillsJson);
|
||||
set => SkillsJson = CareerJson.WriteList(value);
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public List<string> Links
|
||||
{
|
||||
get => CareerJson.ReadList(LinksJson);
|
||||
set => LinksJson = CareerJson.WriteList(value);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CareerCertification : CareerChildEntity
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Issuer { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? Date { get; set; }
|
||||
public string? DateNormalized { get; set; }
|
||||
|
||||
public string DetailsJson { get; set; } = "[]";
|
||||
|
||||
[NotMapped]
|
||||
public List<string> Details
|
||||
{
|
||||
get => CareerJson.ReadList(DetailsJson);
|
||||
set => DetailsJson = CareerJson.WriteList(value);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CareerLanguage : CareerChildEntity
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Level { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
@@ -8,10 +8,29 @@ public sealed class CareerProfile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string OwnerUserId { get; set; } = string.Empty;
|
||||
|
||||
// Phase 3: ProfileJson is now a DERIVED projection of the relational children + LongTailJson,
|
||||
// serialized into the StructuredCvProfile shape and kept in sync on every save so the legacy
|
||||
// read paths (CV rendering, tailoring, match-score) keep working unchanged. It is no longer an
|
||||
// independently editable source. See docs/architecture/career-profile-model.md §4.
|
||||
public string ProfileJson { get; set; } = string.Empty;
|
||||
|
||||
// The editable long tail that is not worth a relational table: contact, summary, interests,
|
||||
// achievements, organisations, publications, courses, custom/other sections, and AI-extraction
|
||||
// metadata. A JSON object (CareerLongTail). The relational children (Experience/Education/Skill/
|
||||
// Project/Certification/Language) hold the rest.
|
||||
public string LongTailJson { get; set; } = string.Empty;
|
||||
|
||||
public int Version { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public List<CareerExperience> Experiences { get; set; } = new();
|
||||
public List<CareerEducation> Education { get; set; } = new();
|
||||
public List<CareerSkill> Skills { get; set; } = new();
|
||||
public List<CareerProject> Projects { get; set; } = new();
|
||||
public List<CareerCertification> Certifications { get; set; } = new();
|
||||
public List<CareerLanguage> Languages { get; set; } = new();
|
||||
}
|
||||
|
||||
// Append-only history: one row per save, so profile edits are never silently lost.
|
||||
|
||||
Reference in New Issue
Block a user