feat: introduce CV variant schema, dual-written from tailored CV saves
Phase F2 of the Career Workspace roadmap: CvVariant, CvVersion, and TailoredApplication -- the reference seam. Per the product boundary, a job application REFERENCES a tailored output; it does not own it. CvVariant is not job-owned: it survives job deletion (SetNull on its optional CareerProfile link, not cascaded), can be reused across applications, and carries its own append-only CvVersion history. TailoredApplication is the join that links a variant to a job (cascades with either side, since the link is meaningless without both). Rather than shipping empty tables with no consumer, this dual-writes from both existing TailoredCvDraft save paths (SaveTailoredCvDraft, UpsertGeneratedTailoredCvDraftAsync via GenerateTailoredCvDraft) -- same pattern as CareerProfile in Phase F1. TailoredCvDraft remains authoritative for every existing read path; the sync is additive and never blocks or fails a draft save. 2 new tests: variant/version/link created on first save, same variant reused (not duplicated) with version incrementing on subsequent saves. Verified against the real dev DB -- FK dependency ordering (CareerProfiles -> CvVariants -> CvVersions/TailoredApplications) holds in both SQLite and MySQL reconciler dialects.
This commit is contained in:
@@ -32,6 +32,9 @@ namespace JobTrackerApi.Data
|
||||
public DbSet<CareerProfileVersion> CareerProfileVersions => Set<CareerProfileVersion>();
|
||||
public DbSet<InterviewPrepNote> InterviewPrepNotes => Set<InterviewPrepNote>();
|
||||
public DbSet<AiWorkspaceNote> AiWorkspaceNotes => Set<AiWorkspaceNote>();
|
||||
public DbSet<CvVariant> CvVariants => Set<CvVariant>();
|
||||
public DbSet<CvVersion> CvVersions => Set<CvVersion>();
|
||||
public DbSet<TailoredApplication> TailoredApplications => Set<TailoredApplication>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -196,6 +199,52 @@ namespace JobTrackerApi.Data
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// CV variants (career-workspace-implementation-roadmap.md Phase F2). A variant is not
|
||||
// owned by a job -- it survives job deletion and can be reused across applications; only
|
||||
// TailoredApplication (the reference) is job-scoped.
|
||||
modelBuilder.Entity<CvVariant>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<CvVariant>()
|
||||
.HasIndex(x => x.OwnerUserId);
|
||||
|
||||
modelBuilder.Entity<CvVariant>()
|
||||
.HasOne(x => x.CareerProfile)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CareerProfileId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<CvVersion>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<CvVersion>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.CvVariantId, x.Version });
|
||||
|
||||
modelBuilder.Entity<CvVersion>()
|
||||
.HasOne(x => x.CvVariant)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CvVariantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<TailoredApplication>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<TailoredApplication>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.JobApplicationId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<TailoredApplication>()
|
||||
.HasOne(x => x.CvVariant)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CvVariantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<TailoredApplication>()
|
||||
.HasOne(x => x.JobApplication)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user