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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user