fix(email): preserve draft send identity
Persist and export each draft's idempotency UUID so refresh and edits cannot create a fresh delivery identity. Add reversible provider-specific migration SQL.
This commit is contained in:
@@ -100,6 +100,7 @@ namespace JobTrackerApi.Controllers
|
||||
draft.Subject,
|
||||
draft.BodyText,
|
||||
draft.ThreadId,
|
||||
draft.ClientRequestId,
|
||||
draft.Revision,
|
||||
draft.CreatedAtUtc,
|
||||
draft.UpdatedAtUtc))
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed class EmailDraftsController(
|
||||
string Subject,
|
||||
string BodyText,
|
||||
string? ThreadId,
|
||||
string ClientRequestId,
|
||||
long Revision,
|
||||
DateTime CreatedAtUtc,
|
||||
DateTime UpdatedAtUtc);
|
||||
@@ -41,15 +42,17 @@ public sealed class EmailDraftsController(
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<DraftDto>>> List(
|
||||
[FromQuery] int jobApplicationId,
|
||||
[FromQuery] int? jobApplicationId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var ownerUserId = GetOwnerUserId();
|
||||
if (ownerUserId is null) return Unauthorized();
|
||||
if (jobApplicationId <= 0) return BadRequest("A valid job application is required.");
|
||||
if (jobApplicationId is <= 0) return BadRequest("A valid job application is required.");
|
||||
|
||||
return Ok(await db.EmailDrafts.AsNoTracking()
|
||||
.Where(draft => draft.OwnerUserId == ownerUserId && draft.JobApplicationId == jobApplicationId)
|
||||
var query = db.EmailDrafts.AsNoTracking().Where(draft => draft.OwnerUserId == ownerUserId);
|
||||
if (jobApplicationId.HasValue)
|
||||
query = query.Where(draft => draft.JobApplicationId == jobApplicationId.Value);
|
||||
return Ok(await query
|
||||
.OrderByDescending(draft => draft.UpdatedAtUtc)
|
||||
.Select(draft => new DraftDto(
|
||||
draft.Id,
|
||||
@@ -59,6 +62,7 @@ public sealed class EmailDraftsController(
|
||||
draft.Subject,
|
||||
draft.BodyText,
|
||||
draft.ThreadId,
|
||||
draft.ClientRequestId,
|
||||
draft.Revision,
|
||||
draft.CreatedAtUtc,
|
||||
draft.UpdatedAtUtc))
|
||||
@@ -103,6 +107,7 @@ public sealed class EmailDraftsController(
|
||||
Subject = subject,
|
||||
BodyText = bodyText,
|
||||
ThreadId = threadId,
|
||||
ClientRequestId = Guid.NewGuid().ToString("D"),
|
||||
Revision = 1,
|
||||
CreatedAtUtc = now,
|
||||
UpdatedAtUtc = now,
|
||||
@@ -193,6 +198,7 @@ public sealed class EmailDraftsController(
|
||||
draft.Subject,
|
||||
draft.BodyText,
|
||||
draft.ThreadId,
|
||||
draft.ClientRequestId,
|
||||
draft.Revision,
|
||||
draft.CreatedAtUtc,
|
||||
draft.UpdatedAtUtc);
|
||||
|
||||
@@ -288,6 +288,7 @@ namespace JobTrackerApi.Data
|
||||
modelBuilder.Entity<EmailDraft>().Property(x => x.To).HasMaxLength(320);
|
||||
modelBuilder.Entity<EmailDraft>().Property(x => x.Subject).HasMaxLength(998);
|
||||
modelBuilder.Entity<EmailDraft>().Property(x => x.ThreadId).HasMaxLength(512);
|
||||
modelBuilder.Entity<EmailDraft>().Property(x => x.ClientRequestId).HasMaxLength(128);
|
||||
modelBuilder.Entity<EmailDraft>()
|
||||
.HasIndex(x => new { x.OwnerUserId, x.JobApplicationId, x.UpdatedAtUtc });
|
||||
modelBuilder.Entity<EmailDraft>()
|
||||
|
||||
+2706
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobTrackerApi.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEmailDraftClientRequestId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
ALTER TABLE `EmailDrafts`
|
||||
ADD `ClientRequestId` varchar(128) NOT NULL DEFAULT '';
|
||||
""");
|
||||
return;
|
||||
}
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ClientRequestId",
|
||||
table: "EmailDrafts",
|
||||
type: "TEXT",
|
||||
maxLength: 128,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClientRequestId",
|
||||
table: "EmailDrafts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1108,6 +1108,11 @@ namespace JobTrackerApi.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientRequestId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class EmailDraft
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string BodyText { get; set; } = string.Empty;
|
||||
public string? ThreadId { get; set; }
|
||||
public string ClientRequestId { get; set; } = string.Empty;
|
||||
public long Revision { get; set; } = 1;
|
||||
public DateTime CreatedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
@@ -24,6 +25,7 @@ public sealed record EmailDraftExport(
|
||||
string Subject,
|
||||
string BodyText,
|
||||
string? ThreadId,
|
||||
string ClientRequestId,
|
||||
long Revision,
|
||||
DateTime CreatedAtUtc,
|
||||
DateTime UpdatedAtUtc);
|
||||
|
||||
@@ -88,6 +88,7 @@ public sealed class DailyExportHostedService(
|
||||
draft.Subject,
|
||||
draft.BodyText,
|
||||
draft.ThreadId,
|
||||
draft.ClientRequestId,
|
||||
draft.Revision,
|
||||
draft.CreatedAtUtc,
|
||||
draft.UpdatedAtUtc))
|
||||
|
||||
Reference in New Issue
Block a user