feat(career): CV builder backend — data-driven theme engine + variant model
Phase 4 foundation. A CvVariant is a lens over the master CareerProfile
(section order/visibility, per-item overrides keyed by ItemKey, theme +
builder settings) — it references career data, never duplicates it. One
renderer (ThemedCvRenderer) draws every theme; a theme is pure data
(CvThemeCatalog, 8 professional themes), so adding a theme needs no renderer
change. Autosave version history + non-destructive restore, public CV via
/api/public-cv/{slug} (anonymous, noindex, filter-bypassing owner load), and
an AI-assist endpoint reusing the existing provider abstraction (suggestions
only, never auto-applied).
- Models: CvVariant/CvVariantVersion, CvVariantSettings, CvTheme + catalog
- Services: CvVariantResolver (profile+lens -> render model), ThemedCvRenderer,
CvVariantService, CareerProfileService.LoadStructuredForOwnerAsync (public)
- API: CvVariantController (/api/cv), PublicCvController (/api/public-cv)
- Migration AddCvVariants (2 self-contained tables; verified applied on the
running container), 16 tests (resolver/renderer/service), 296 backend green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,8 @@ namespace JobTrackerApi.Data
|
||||
public DbSet<CareerLanguage> CareerLanguages => Set<CareerLanguage>();
|
||||
public DbSet<InterviewPrepNote> InterviewPrepNotes => Set<InterviewPrepNote>();
|
||||
public DbSet<AiWorkspaceNote> AiWorkspaceNotes => Set<AiWorkspaceNote>();
|
||||
public DbSet<CvVariant> CvVariants => Set<CvVariant>();
|
||||
public DbSet<CvVariantVersion> CvVariantVersions => Set<CvVariantVersion>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -288,6 +290,33 @@ namespace JobTrackerApi.Data
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Phase 4: CV variants (a lens over the master profile) + their autosave history.
|
||||
// docs/architecture/cv-builder.md. Same deny-on-null tenant filter as everything else. A
|
||||
// variant references a job application (reference-not-ownership, SetNull on delete) and never
|
||||
// owns career data. PublicSlug is globally unique so /cv/{slug} can resolve it anonymously.
|
||||
modelBuilder.Entity<CvVariant>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
modelBuilder.Entity<CvVariant>()
|
||||
.HasIndex(x => x.PublicSlug)
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<CvVariant>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.UpdatedAtUtc });
|
||||
modelBuilder.Entity<CvVariant>()
|
||||
.HasOne(x => x.JobApplication)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<CvVariantVersion>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.OwnerUserId == CurrentUserId);
|
||||
modelBuilder.Entity<CvVariantVersion>()
|
||||
.HasIndex(x => new { x.CvVariantId, x.Version });
|
||||
modelBuilder.Entity<CvVariantVersion>()
|
||||
.HasOne(x => x.CvVariant)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CvVariantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
// Common config for CareerProfile's relational children. The 1:many FK + cascade delete is
|
||||
|
||||
Reference in New Issue
Block a user