refactor(api): extract ProfileCv DTOs, add missing AsNoTracking on reads
This commit is contained in:
@@ -123,7 +123,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
var result = await controller.GetRuns();
|
var result = await controller.GetRuns();
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
var runs = Assert.IsAssignableFrom<IEnumerable<ProfileCvController.CvExtractionRunListItem>>(ok.Value);
|
var runs = Assert.IsAssignableFrom<IEnumerable<CvExtractionRunListItem>>(ok.Value);
|
||||||
var single = Assert.Single(runs);
|
var single = Assert.Single(runs);
|
||||||
Assert.Equal("upload", single.Trigger);
|
Assert.Equal("upload", single.Trigger);
|
||||||
Assert.Equal("applied", single.Status);
|
Assert.Equal("applied", single.Status);
|
||||||
@@ -611,7 +611,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
|
|
||||||
var objectResult = Assert.IsType<ObjectResult>(result);
|
var objectResult = Assert.IsType<ObjectResult>(result);
|
||||||
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
|
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
|
||||||
var payload = Assert.IsType<ProfileCvController.CvRewriteFailureDto>(objectResult.Value);
|
var payload = Assert.IsType<CvRewriteFailureDto>(objectResult.Value);
|
||||||
Assert.Equal("ai-service-unavailable", payload.Code);
|
Assert.Equal("ai-service-unavailable", payload.Code);
|
||||||
Assert.Contains("could not rewrite", payload.Message, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("could not rewrite", payload.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Contains("unavailable", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("unavailable", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -673,7 +673,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
|
|
||||||
var objectResult = Assert.IsType<ObjectResult>(result);
|
var objectResult = Assert.IsType<ObjectResult>(result);
|
||||||
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
|
Assert.Equal(StatusCodes.Status502BadGateway, objectResult.StatusCode);
|
||||||
var payload = Assert.IsType<ProfileCvController.CvRewriteFailureDto>(objectResult.Value);
|
var payload = Assert.IsType<CvRewriteFailureDto>(objectResult.Value);
|
||||||
Assert.Equal("rewrite-empty", payload.Code);
|
Assert.Equal("rewrite-empty", payload.Code);
|
||||||
Assert.Contains("empty", payload.Message, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("empty", payload.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Contains("no usable text", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("no usable text", payload.Detail ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -766,7 +766,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
var paths = CreatePaths();
|
var paths = CreatePaths();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
||||||
|
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(user.ProfileCvText));
|
var result = await controller.Parse(new ParseCvRequest(user.ProfileCvText));
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
var json = JsonSerializer.Serialize(ok.Value);
|
var json = JsonSerializer.Serialize(ok.Value);
|
||||||
@@ -800,7 +800,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
var paths = CreatePaths();
|
var paths = CreatePaths();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
||||||
|
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(user.ProfileCvText));
|
var result = await controller.Parse(new ParseCvRequest(user.ProfileCvText));
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
var json = JsonSerializer.Serialize(ok.Value);
|
var json = JsonSerializer.Serialize(ok.Value);
|
||||||
@@ -838,7 +838,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
var paths = CreatePaths();
|
var paths = CreatePaths();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
|
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
|
||||||
|
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
var json = JsonSerializer.Serialize(ok.Value);
|
var json = JsonSerializer.Serialize(ok.Value);
|
||||||
@@ -878,7 +878,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
var paths = CreatePaths();
|
var paths = CreatePaths();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
|
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
|
||||||
|
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
var json = JsonSerializer.Serialize(ok.Value);
|
var json = JsonSerializer.Serialize(ok.Value);
|
||||||
@@ -914,7 +914,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
var paths = CreatePaths();
|
var paths = CreatePaths();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
|
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
|
||||||
|
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
var json = JsonSerializer.Serialize(ok.Value);
|
var json = JsonSerializer.Serialize(ok.Value);
|
||||||
@@ -1030,7 +1030,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
|
|
||||||
await using var db = CreateDb();
|
await using var db = CreateDb();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths(), null, normalizer.Object);
|
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths(), null, normalizer.Object);
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
Assert.IsType<OkObjectResult>(result.Result);
|
Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
|
||||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||||
@@ -1069,7 +1069,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
var paths = CreatePaths();
|
var paths = CreatePaths();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
||||||
|
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(rawSource));
|
var result = await controller.Parse(new ParseCvRequest(rawSource));
|
||||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
Assert.NotNull(ok.Value);
|
Assert.NotNull(ok.Value);
|
||||||
|
|
||||||
@@ -1098,7 +1098,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
|
|
||||||
await using var db = CreateDb();
|
await using var db = CreateDb();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
Assert.IsType<OkObjectResult>(result.Result);
|
Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
|
||||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||||
@@ -1129,7 +1129,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
|
|
||||||
await using var db = CreateDb();
|
await using var db = CreateDb();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
Assert.IsType<OkObjectResult>(result.Result);
|
Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
|
||||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||||
@@ -1158,7 +1158,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
|
|
||||||
await using var db = CreateDb();
|
await using var db = CreateDb();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
Assert.IsType<OkObjectResult>(result.Result);
|
Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
|
||||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||||
@@ -1186,7 +1186,7 @@ public sealed class ProfileCvControllerTests
|
|||||||
|
|
||||||
await using var db = CreateDb();
|
await using var db = CreateDb();
|
||||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
var result = await controller.Parse(new ParseCvRequest(source));
|
||||||
Assert.IsType<OkObjectResult>(result.Result);
|
Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
|
||||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||||
|
|||||||
@@ -113,25 +113,8 @@ public sealed class ProfileCvController : ControllerBase
|
|||||||
public string? Tone { get; set; }
|
public string? Tone { get; set; }
|
||||||
public string? Language { get; set; }
|
public string? Language { get; set; }
|
||||||
}
|
}
|
||||||
public sealed record ParseCvRequest(string? Text);
|
|
||||||
public sealed record CvTemplateDescriptor(string Id, string Title, string Tone, string AccentColor, string PreviewTagline, string PreviewSummary, List<string> PreviewBullets);
|
|
||||||
public sealed record ProfileCvPreviewDto(string TemplateId, string Html, string SuggestedFileName, string FullText, string RewrittenText, string? SectionName, StructuredCvProfile StructuredCv, TailoredCvDocument Document, string? TargetRole, int? JobApplicationId);
|
|
||||||
public sealed record CvRewriteFailureDto(string Code, string Message, string? Detail = null, string? LastAiError = null);
|
|
||||||
|
|
||||||
private sealed record ExtractionPipelineResult(string RawText, string NormalizedText, StructuredCvProfile StructuredCv);
|
private sealed record ExtractionPipelineResult(string RawText, string NormalizedText, StructuredCvProfile StructuredCv);
|
||||||
private sealed record ClassifiedCvBlock(int Index, string OriginalBlock, string SectionName, string Content, CvBlockClassificationResult? Classification);
|
private sealed record ClassifiedCvBlock(int Index, string OriginalBlock, string SectionName, string Content, CvBlockClassificationResult? Classification);
|
||||||
public sealed record CvExtractionRunListItem(
|
|
||||||
int Id,
|
|
||||||
string Trigger,
|
|
||||||
string Status,
|
|
||||||
string? ArtifactFileName,
|
|
||||||
DateTimeOffset StartedAtUtc,
|
|
||||||
DateTimeOffset? CompletedAtUtc,
|
|
||||||
DateTimeOffset? AppliedAtUtc,
|
|
||||||
string ParserVersion,
|
|
||||||
string NormalizerVersion,
|
|
||||||
string LlmPromptVersion,
|
|
||||||
string? ErrorMessage);
|
|
||||||
|
|
||||||
[HttpPost("upload")]
|
[HttpPost("upload")]
|
||||||
[RequestSizeLimit(MaxFileSizeBytes)]
|
[RequestSizeLimit(MaxFileSizeBytes)]
|
||||||
@@ -254,6 +237,7 @@ public sealed class ProfileCvController : ControllerBase
|
|||||||
if (user is null) return Unauthorized();
|
if (user is null) return Unauthorized();
|
||||||
|
|
||||||
var artifact = await _db.CvUploadArtifacts
|
var artifact = await _db.CvUploadArtifacts
|
||||||
|
.AsNoTracking()
|
||||||
.OrderByDescending(x => x.UploadedAtUtc)
|
.OrderByDescending(x => x.UploadedAtUtc)
|
||||||
.FirstOrDefaultAsync(x => x.OwnerUserId == user.Id, HttpContext.RequestAborted);
|
.FirstOrDefaultAsync(x => x.OwnerUserId == user.Id, HttpContext.RequestAborted);
|
||||||
|
|
||||||
@@ -941,7 +925,7 @@ public sealed class ProfileCvController : ControllerBase
|
|||||||
}
|
}
|
||||||
case "reprocess":
|
case "reprocess":
|
||||||
{
|
{
|
||||||
var artifact = await _db.CvUploadArtifacts.FirstOrDefaultAsync(x => x.Id == run.ArtifactId && x.OwnerUserId == user.Id, cancellationToken);
|
var artifact = await _db.CvUploadArtifacts.AsNoTracking().FirstOrDefaultAsync(x => x.Id == run.ArtifactId && x.OwnerUserId == user.Id, cancellationToken);
|
||||||
if (artifact is null) throw new InvalidOperationException("Upload a CV before reprocessing it.");
|
if (artifact is null) throw new InvalidOperationException("Upload a CV before reprocessing it.");
|
||||||
if (string.IsNullOrWhiteSpace(artifact.StoragePath) || !System.IO.File.Exists(artifact.StoragePath))
|
if (string.IsNullOrWhiteSpace(artifact.StoragePath) || !System.IO.File.Exists(artifact.StoragePath))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using JobTrackerApi.Models;
|
||||||
|
|
||||||
|
namespace JobTrackerApi.Controllers;
|
||||||
|
|
||||||
|
public sealed record ParseCvRequest(string? Text);
|
||||||
|
public sealed record CvTemplateDescriptor(string Id, string Title, string Tone, string AccentColor, string PreviewTagline, string PreviewSummary, List<string> PreviewBullets);
|
||||||
|
public sealed record ProfileCvPreviewDto(string TemplateId, string Html, string SuggestedFileName, string FullText, string RewrittenText, string? SectionName, StructuredCvProfile StructuredCv, TailoredCvDocument Document, string? TargetRole, int? JobApplicationId);
|
||||||
|
public sealed record CvRewriteFailureDto(string Code, string Message, string? Detail = null, string? LastAiError = null);
|
||||||
|
public sealed record CvExtractionRunListItem(
|
||||||
|
int Id,
|
||||||
|
string Trigger,
|
||||||
|
string Status,
|
||||||
|
string? ArtifactFileName,
|
||||||
|
DateTimeOffset StartedAtUtc,
|
||||||
|
DateTimeOffset? CompletedAtUtc,
|
||||||
|
DateTimeOffset? AppliedAtUtc,
|
||||||
|
string ParserVersion,
|
||||||
|
string NormalizerVersion,
|
||||||
|
string LlmPromptVersion,
|
||||||
|
string? ErrorMessage);
|
||||||
Reference in New Issue
Block a user